Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create dynamic validation schema. #559

Closed
vijayranghar opened this issue Jun 23, 2019 · 30 comments
Closed

Create dynamic validation schema. #559

vijayranghar opened this issue Jun 23, 2019 · 30 comments

Comments

@vijayranghar
Copy link

vijayranghar commented Jun 23, 2019

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

@akmjenkins
Copy link
Contributor

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.

@vijayranghar
Copy link
Author

vijayranghar commented Aug 7, 2019

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

@ybv
Copy link

ybv commented Aug 20, 2019

@vijayranghar this approach is solid! thanks for sharing

@bishoymelek-zz
Copy link

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

@Shaker-Hamdi
Copy link

@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.

@Shaker-Hamdi
Copy link

Shaker-Hamdi commented Sep 19, 2019

@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
Copy link
Author

@Shaker-Hamdi https://codesandbox.io/s/clever-snyder-1u410?fontsize=14

@saurabhnemade
Copy link

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

@CodeSchneider
Copy link

@vijayranghar this is elegant. Thank you for sharing.

@ljhyeok
Copy link

ljhyeok commented Mar 17, 2020

@vijayranghar thank you!!

@lookfirst
Copy link

lookfirst commented Mar 25, 2020

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;
}

@ritchieanesco
Copy link

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

@lookfirst
Copy link

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?

@ritchieanesco
Copy link

ritchieanesco commented Apr 2, 2020

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.

@lookfirst
Copy link

@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.

@akhilaravind
Copy link

@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; }), });

@thealexandrelara
Copy link

@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)

@gmonte
Copy link

gmonte commented Aug 28, 2020

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()
  })
})

@Cuantico
Copy link

Cuantico commented Sep 8, 2020

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

@Waqqars987
Copy link

Waqqars987 commented Nov 6, 2020

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.

@developdeez
Copy link

developdeez commented Jan 6, 2021

@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
Copy link

@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.

@developdeez
Copy link

@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

@WaqqarSuleman
Copy link

@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."]}],
}

@WaqqarSuleman
Copy link

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

@scrapecoder
Copy link

scrapecoder commented Mar 10, 2021

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?

@dclark27
Copy link

dclark27 commented Apr 1, 2021

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

@akhilaravind
Copy link

@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

@akalana
Copy link

akalana commented Dec 2, 2022

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

@consciousnessdev
Copy link

consciousnessdev commented Dec 3, 2023

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]
      }
    ]
  },
...
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests