jquense / yup

Dead simple Object schema validation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Create dynamic validation schema.

vijayranghar opened this issue · comments

I have a JSON from which I'm trying to create dynamic form and validation. I'm using Yup along with Formik. I want to create the validation schema after iterating over the form elements validation key but I'm not sure how to do this.

export const formData = [
  {
    id: "name",
    label: "Full name",
    placeholder: "Enter full name",
    type: "text",
    required: true,
    value: "User name",
    values: [],
    validations: [
      {
        type: "minLength",
        value: "5",
        error_message: "name should be atleast 5 char long"
      },
      {
        type: "maxLength",
        value: "10",
        error_message: "name should be atleast 5 char long"
      }
    ]
  },
  {
    id: "email",
    label: "Email",
    placeholder: "Email",
    type: "text",
    required: true,
    value: "email",
    values: [],
    validations: [
      {
        type: "minLength",
        value: "5",
        error_message: "name should be atleast 5 char long"
      },
      {
        type: "maxLength",
        value: "10",
        error_message: "name should be atleast 5 char long"
      },
      {
        type: "email",
        error_message: "Valid email"
      }
    ]
  },
  {
    id: "phoneNumber",
    label: "phone number",
    type: "text",
    required: true,
    value: "7878787878",
    values: [],
    validations: [
      {
        type: "minLength",
        value: "5",
        error_message: "name should be atleast 5 char long"
      },
      {
        type: "maxLength",
        value: "10",
        error_message: "name should be atleast 5 char long"
      },
      {
        type: "required",
        error_message: "phone number is required"
      }
    ]
  },
  {
    id: "total",
    label: "Total People in Family",
    placeholder: "family members count",
    type: "text",
    required: false,
    value: "1",
    values: [],
    validations: [
      {
        type: "minLength",
        value: "1",
        error_message: "there should be atleast 1 family member"
      },
      {
        type: "maxLength",
        value: "5",
        error_message: "max family members can be 5"
      }
    ]
  }
]

I want to create something like after iterating over the validation but I'm unable to do so.

let validateSchema = yup.object().shape({
    name: yup.string().required("name is required"),
    email: yup.string().email(),
    phoneNumber: yup.number().min(10, "minium 10 numbers"),
    total: yup
      .number()
      .min(1, "minium 1 member")
      .max(5, "max 5 member")
      .required("member is required")    
})

Which I will send to Formik. I looked into many links, even posted on SO but couldn't find any useful lead.

Codesandbox

commented

This is the only open source one available right now - https://www.npmjs.com/package/json-schema-to-yup I'm working on something similar because it didn't suit my needs, but it's not ready for open source yet.

In case someone is trying to create yupschema on the fly. With some help, I was able to do it without using any external package.

import * as yup from "yup";

export function createYupSchema(schema, config) {
  const { id, validationType, validations = [] } = config;
  if (!yup[validationType]) {
    return schema;
  }
  let validator = yup[validationType]();
  validations.forEach(validation => {
    const { params, type } = validation;
    if (!validator[type]) {
      return;
    }
    console.log(type, params);
    validator = validator[type](...params);
  });
  schema[id] = validator;
  return schema;
}

codesandbox

@vijayranghar this approach is solid! thanks for sharing

Thanks @vijayranghar for sharing it. was looking for that exact way to solve it.

@vijayranghar Great example, just one thing I can't get my head around is this ...
createYupSchema(schema, config)
Where does the "schema" come from?
In the form.js you reduce like this ...
const yepSchema = formData.reduce(createYupSchema, {});
But I just can't figure it out.
It would be great if you could explain it.

@vijayranghar one more question, how to add to an existing validation schema? Let's say I already have two inputs inside the form that doesn't get generated dynamically and they already have validation inside an existing schema, how to add to that the dynamic validation?

@vijayranghar is there any way to support object and array in implementation?

@vijayranghar this is elegant. Thank you for sharing.

Edit. Just a little any makes everybody happy.

import * as yup from "yup";

export function createYupSchema(schema: any, config: any) {
	const {id, validationType, validations = []} = config;

	if (!(yup as any)[validationType]) {
		return schema;
	}

	let validator = (yup as any)[validationType]();

	validations.forEach((validation: any) => {
		const {params, type} = validation;
		if (!validator[type]) {
			return;
		}
		validator = validator[type](...params);
	});

	schema[id] = validator;
	return schema;
}

Although this issue is resolved.. for those looking for a way to do this via a utility I just released this: https://www.npmjs.com/package/json-schema-yup-transformer

Although this issue is resolved.. for those looking for a way to do this via a utility I just released this: https://www.npmjs.com/package/json-schema-yup-transformer

Can you explain how your project is different / better / more appealing than the existing one? https://www.npmjs.com/package/json-schema-to-yup

Competition is good, but why not just work with the existing one to improve it?

I’ve contributed to schema to yup, it’s a good project...however decided to build my own because:

  1. Does not adhere to the draft 7 spec
  2. Lacks good code coverage
  3. The code is hard to read and debug
  4. It feels like it’s more catered to making schemas work with yup as opposed to adhering to schema specifications

The main difference I see with schema to yup is that is intended to work with all sorts of schemas but does not strictly adhere to their rules.

Hope that helps.

@ritchieanesco Fantastic reasons! You should add... written in typescript. =)

I just took a look and you've got nice test coverage. I'll definitely go with yours when I need it. Keep up the great work.

@lookfirst how can we add conditional validation here, like
Yup.object().shape({ email: Yup.string().email('Invalid email address').required('Email is required!'), username: Yup.string().required('This man needs a username').when('email', (email, schema) => { if (email === 'foobar@example.com') { return schema.min(10); } return schema; }), });

@lookfirst how can we add conditional validation here, like
Yup.object().shape({ email: Yup.string().email('Invalid email address').required('Email is required!'), username: Yup.string().required('This man needs a username').when('email', (email, schema) => { if (email === 'foobar@example.com') { return schema.min(10); } return schema; }), });

For this to work your config should look like this:

const exampleFieldConfig = {
    id: "username",
    label: "Username",
    placeholder: "username",
    type: "text",
    validationType: "string",
    required: true,
    value: undefined,
    validations: [
      {
        type: "required",
        params: ["this field is required"]
      },
      {
        type: "min",
        params: [5, "current min: 5 characters"]
      },
      {
        type: "when",
        params: [
          "email",
          (email, schema) => {
            return email === "foobar@example.com" ? schema.min(10, "current min: 10 characters") : schema;
          }
        ]
      }
    ]
  }

Then you can pass it to the function:

createYupSchema({}, exampleFieldConfig)

Hi everyone!
In my case, I don't want write a lot of code to do the nested validations that Yup do "automatically".
So I resolved the dynamic validation creating a function that receive the data to validate, and in the field that I want change the validation, I execute an lambda function that return the corresponding new Yup instance.

import * as Yup from 'yup'

const schemaGenerator = data => Yup.object().shape({
  name: Yup.string().required('this field is required'),
  email: Yup.string().email('enter a valid email').required('this field is required'),
  address: Yup.object().shape({
    city: Yup.string().required('this field is required'),
    neighborhood: (() => {
      let validation = Yup.string()
      if (data?.address?.street) {
        validation = validation.required('this field is required')
      }
      return validation
    })(), // lambda function here to create neighborhood schema dinamically!
    street: Yup.string()
  })
})

So, to:

schemaGenerator({
  name: '',
  email: '',
  address: {
    city: '',
    neighborhood: '',
    street: ''
  }
})

I have corresponding Yup schema:

Yup.object().shape({
  name: Yup.string().required('this field is required'),
  email: Yup.string().email('enter a valid email').required('this field is required'),
  address: Yup.object().shape({
    city: Yup.string().required('this field is required'),
    neighborhood: Yup.string(),
    street: Yup.string()
  })
})

And to:

schemaGenerator({
  name: '',
  email: '',
  address: {
    city: '',
    neighborhood: '',
    street: 'something'
  }
})

I have corresponding Yup schema:

Yup.object().shape({
  name: Yup.string().required('this field is required'),
  email: Yup.string().email('enter a valid email').required('this field is required'),
  address: Yup.object().shape({
    city: Yup.string().required('this field is required'),
    neighborhood: Yup.string().required('this field is required'),
    street: Yup.string()
  })
})

@vijayranghar thas is nice!!! thank you very much, do you know how to add a nullable validation to a date field with this approach?

In case someone is trying to create yupschema on the fly. With some help, I was able to do it without using any external package.

import * as yup from "yup";

export function createYupSchema(schema, config) {
  const { id, validationType, validations = [] } = config;
  if (!yup[validationType]) {
    return schema;
  }
  let validator = yup[validationType]();
  validations.forEach(validation => {
    const { params, type } = validation;
    if (!validator[type]) {
      return;
    }
    console.log(type, params);
    validator = validator[type](...params);
  });
  schema[id] = validator;
  return schema;
}

codesandbox

Hi,

I have the following schema requirement:

validationSchema: Yup.object({
		dialCode: Yup.string().required('Required'),
		mobileNumber: Yup.string().trim().when('dialCode', (dialCode, schema) => {
			return schema
				.phone(getAlpha2Code(dialCode), true, 'Phone number is invalid')
				.required('Phone number is required');
		})
	}),

getAlpha2Code is function which returns the Alpha2Code of a particular country from its dial code.
How do I create a JSON schema for this particular Yup schema? What should the validations array look like, especially for the mobileNumber?
Please help.

@Waqqars987 I was wondering the same. I have a JSON that contains the inputs and although Im able to get min and max working. Required never seems to pickup

import * as yup from "yup";

export default function createYupSchema(schema, config) {
  const { id, validationType, validations = [] } = config;
  if (!yup[validationType]) {
    return schema;
  }
  let validator = yup[validationType]();
  validations.forEach((validation) => {
    const { type, value, msg } = validation;
    if (!validator[type]) {
      return;
    }
    validator = validator[type](value, msg);
  });
  schema[id] = validator;
  return schema;
}

Input:

 "inputs": [
      {
        "id": "name",
        "type": "text",
        "label": "Name",
        "validationType": "string",
        "validations": [
          {
            "type": "required",
            "msg": "this field is required"
          },
          {
            "type": "min",
            "value": 5,
            "msg": "name cannot be less than 5 characters"
          },
          {
            "type": "max",
            "value": 10,
            "msg": "name cannot be more than 10 characters"
          }
        ]
      }]
      ```

@Waqqars987 I was wondering the same. I have a JSON that contains the inputs and although Im able to get min and max working. Required never seems to pickup

import * as yup from "yup";

export default function createYupSchema(schema, config) {
  const { id, validationType, validations = [] } = config;
  if (!yup[validationType]) {
    return schema;
  }
  let validator = yup[validationType]();
  validations.forEach((validation) => {
    const { type, value, msg } = validation;
    if (!validator[type]) {
      return;
    }
    validator = validator[type](value, msg);
  });
  schema[id] = validator;
  return schema;
}

Required does work for me. However, I can't figure out how to make other Yup functions work as per my requirements.

@Waqqars987 I was wondering the same. I have a JSON that contains the inputs and although Im able to get min and max working. Required never seems to pickup

import * as yup from "yup";

export default function createYupSchema(schema, config) {
  const { id, validationType, validations = [] } = config;
  if (!yup[validationType]) {
    return schema;
  }
  let validator = yup[validationType]();
  validations.forEach((validation) => {
    const { type, value, msg } = validation;
    if (!validator[type]) {
      return;
    }
    validator = validator[type](value, msg);
  });
  schema[id] = validator;
  return schema;
}

Required does work for me. However, I can't figure out how to make other Yup functions work as per my requirements.

What shape is your JSON? I've attached mine above

@Waqqars987 I was wondering the same. I have a JSON that contains the inputs and although Im able to get min and max working. Required never seems to pickup

import * as yup from "yup";

export default function createYupSchema(schema, config) {
  const { id, validationType, validations = [] } = config;
  if (!yup[validationType]) {
    return schema;
  }
  let validator = yup[validationType]();
  validations.forEach((validation) => {
    const { type, value, msg } = validation;
    if (!validator[type]) {
      return;
    }
    validator = validator[type](value, msg);
  });
  schema[id] = validator;
  return schema;
}

Required does work for me. However, I can't figure out how to make other Yup functions work as per my requirements.

What shape is your JSON? I've attached mine above

{
id: "label",
label: "Field Label",
placeholder: "Enter Label",
control: "text",
validationType: "string",
validations: [{type: "required", params: ["Field Label is required."]}],
}

What should the object for yup.array().of(yup.string()) validation look like?

how to validate the two fields value are same or not when creating dynamic yup object
eg - password and confirm password

for static i can use -
password: yup.string().required('Password is required'), confirmPassword: yup.string().oneOf([yup.ref('password'), null], "Passwords don't match").required('Confirm Password is required')

but how to create this object dynamic?

Just wanted to share an expanded version of all of the great ideas in here. This works with nested objects, as well as arrays.
It also supports custom messages for default errors with the validationTypeError prop.

I'm sure there is a shorter way to do this, but I haven't gotten around to refactoring yet. It does have a bunch of tests but I'll update as I find issues. This thread really saved my butt!

It takes an array of fields like so:

 const fields = [
  {
    name: 'applicant.0.firstName', // in an array
    label: 'First name',
    validationType: 'string',
    validations: [
      {
        type: 'required',
        params: ['Required']
      }
    ]
  },
  {
    name: 'person.lastName', // nested in an object
    label: 'Last name',
    validationType: 'string',
    validations: [
      {
        type: 'required',
        params: ['Required']
      }
    ]
  },
    {
    name: 'email', // non nested, non array
    label: 'Email',
    validationType: 'string',
    validations: [
      {
        type: 'required',
        params: ['Required']
      }
    ]
  }
]

and returns this:

yup.object().shape({
  applicant: yup.array().of(yup.object.shape({
    firstName: yup.string().required()
  })),
  person: yup.object().shape({
    lastName: yup.string().required()
  }),
  email: yup.string().required()
})

Here is the source:

import * as yup from 'yup'

const getValidationSchema = (fields) => {
  const schema = fields.reduce((schema, field) => {
    const { name, validationType, validationTypeError, validations = [] } = field
    const isObject = name.indexOf('.') >= 0

    if (!yup[validationType]) {
      return schema
    }
    let validator = yup[validationType]().typeError(validationTypeError || '')
    validations.forEach(validation => {
      const { params, type } = validation
      if (!validator[type]) {
        return
      }
      validator = validator[type](...params)
    })

    if (!isObject) {
      return schema.concat(yup.object().shape({ [name]: validator }))
    }

    const reversePath = name.split('.').reverse()
    const currNestedObject = reversePath.slice(1).reduce((yupObj, path, index, source) => {
      if (!isNaN(path)) {
        return { array: yup.array().of(yup.object().shape(yupObj)) }
      }
      if (yupObj.array) {
        return { [path]: yupObj.array }
      }
      return { [path]: yup.object().shape(yupObj) }
    }, { [reversePath[0]]: validator })

    const newSchema = yup.object().shape(currNestedObject)
    return schema.concat(newSchema)
  }, yup.object().shape({}))

  return schema
}

export default getValidationSchema

@lookfirst how can we add conditional validation here, like
Yup.object().shape({ email: Yup.string().email('Invalid email address').required('Email is required!'), username: Yup.string().required('This man needs a username').when('email', (email, schema) => { if (email === 'foobar@example.com') { return schema.min(10); } return schema; }), });

For this to work your config should look like this:

const exampleFieldConfig = {
    id: "username",
    label: "Username",
    placeholder: "username",
    type: "text",
    validationType: "string",
    required: true,
    value: undefined,
    validations: [
      {
        type: "required",
        params: ["this field is required"]
      },
      {
        type: "min",
        params: [5, "current min: 5 characters"]
      },
      {
        type: "when",
        params: [
          "email",
          (email, schema) => {
            return email === "foobar@example.com" ? schema.min(10, "current min: 10 characters") : schema;
          }
        ]
      }
    ]
  }

Then you can pass it to the function:

createYupSchema({}, exampleFieldConfig)

This is not working and it returns a cyclic dependency error

Hey, Is there any suggestion for converting Joi description into the Yup ?

Just wanted to share an expanded version of all of the great ideas in here. This works with nested objects, as well as arrays. It also supports custom messages for default errors with the validationTypeError prop.

I'm sure there is a shorter way to do this, but I haven't gotten around to refactoring yet. It does have a bunch of tests but I'll update as I find issues. This thread really saved my butt!

It takes an array of fields like so:

 const fields = [
  {
    name: 'applicant.0.firstName', // in an array
    label: 'First name',
    validationType: 'string',
    validations: [
      {
        type: 'required',
        params: ['Required']
      }
    ]
  },
  {
    name: 'person.lastName', // nested in an object
    label: 'Last name',
    validationType: 'string',
    validations: [
      {
        type: 'required',
        params: ['Required']
      }
    ]
  },
    {
    name: 'email', // non nested, non array
    label: 'Email',
    validationType: 'string',
    validations: [
      {
        type: 'required',
        params: ['Required']
      }
    ]
  }
]

and returns this:

yup.object().shape({
  applicant: yup.array().of(yup.object.shape({
    firstName: yup.string().required()
  })),
  person: yup.object().shape({
    lastName: yup.string().required()
  }),
  email: yup.string().required()
})

Here is the source:

import * as yup from 'yup'

const getValidationSchema = (fields) => {
  const schema = fields.reduce((schema, field) => {
    const { name, validationType, validationTypeError, validations = [] } = field
    const isObject = name.indexOf('.') >= 0

    if (!yup[validationType]) {
      return schema
    }
    let validator = yup[validationType]().typeError(validationTypeError || '')
    validations.forEach(validation => {
      const { params, type } = validation
      if (!validator[type]) {
        return
      }
      validator = validator[type](...params)
    })

    if (!isObject) {
      return schema.concat(yup.object().shape({ [name]: validator }))
    }

    const reversePath = name.split('.').reverse()
    const currNestedObject = reversePath.slice(1).reduce((yupObj, path, index, source) => {
      if (!isNaN(path)) {
        return { array: yup.array().of(yup.object().shape(yupObj)) }
      }
      if (yupObj.array) {
        return { [path]: yupObj.array }
      }
      return { [path]: yup.object().shape(yupObj) }
    }, { [reversePath[0]]: validator })

    const newSchema = yup.object().shape(currNestedObject)
    return schema.concat(newSchema)
  }, yup.object().shape({}))

  return schema
}

export default getValidationSchema

How to validate array length?

example data:

const fields = [
  {
    name: 'applicant.0.arrayOfName', // is an array
    label: 'List Of First name',
    validationType: 'array',  // like this, or how?
    validations: [
      {
        type: 'min',
        params: [1, 'Required'] // [length, message]
      }
    ]
  },
...
]