How to validate individual element of an array of JSON objects using Yup
Asked Answered
P

3

5

I have a JSON object which contains an array of JSON objects. My need is out of 2 array elements (vehicles), I just need to ensure that at least one is filled with data i.e. both can't be empty.. see my declaration below and my Yum schema

 const initialValues = {
  applicantID: "",
  applicantTypeID: "1",
  firstName: "",
  lastName: "",
  email: "[email protected]",
  address1: "",
  address2: "",
  suburb: "",
  state: "AZ",
  postcode: "",
  phone: "",
  mobile: "",
  business_unit_school: "",
  building: "",
  level: "",
  room: "",
  applicationStatusID: "1",
  **vehicles: [
    { registrationNumber: "", make: "", model: "" },
    { registrationNumber: "", make: "", model: "" },
  ],**
  
};

const validationSchema = Yup.object({
  applicantID: Yup.string().required("Employee Number required."),
  firstName: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("Firstname required."),
  lastName: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("Lastname required."),
  email: Yup.string().email("Invalid email format").required("Email required."),
  address1: Yup.string()
    .min(2, "Too short.")
    .max(255, "Too Long!")
    .required("Address required."),
  address2: Yup.string().max(255, "Max 255 characters allowed."),
  suburb: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("Suburb required."),
  state: Yup.string()
    .min(2, "Too Short!")
    .max(30, "Max 30 characters allowed.")
    .required("State required."),
  business_unit_school: Yup.string()
    .min(2, "Too Short!")
    .max(100, "Max 100 characters allowed.")
    .required("Business unit required."),
  **vehicles: Yup.array().of(
    Yup.object().shape({
      registrationNumber: Yup.string().required("Required"),
    })
  ),**
postcode: Yup.string().required("Postcode required."),
  phone: Yup.number()
    .required("Phone number required")
    .typeError("You must specify a number"),
  mobile: Yup.number().required("").typeError("You must specify a number"),
});

My above vehicles validation works though it forces user to fill in registrationNumber element of both array items under vehicle which is not I want. Any help would be much appreciated. I also tried below and it doesn't work ...

let vehicleschema = Yup.object({
  vehicles: Yup.array().of(
    Yup.object({
      registrationNumber: Yup.string().required("Required"),
      make: Yup.string().required("Required"),
    })
  ),
});

const validationSchema = Yup.object({
vehicles: vehicleschema.validateAt("vehicles[0].registrationNumber", initialValues)
}),

I get below error on validation ...

TypeError: field.resolve is not a function
(anonymous function)
node_modules/yup/es/object.js:146
  143 | 
  144 | innerOptions.path = makePath(_templateObject(), options.path, prop);
  145 | innerOptions.value = value[prop];
> 146 | field = field.resolve(innerOptions);
      | ^  147 | 
  148 | if (field._strip === true) {
  149 |   isChanged = isChanged || prop in value;
View compiled
ObjectSchema._cast
node_modules/yup/es/object.js:136
  133 | });
  134 | 
  135 | var isChanged = false;
> 136 | props.forEach(function (prop) {
      | ^  137 |   var field = fields[prop];
  138 |   var exists = has(value, prop);
  139 | 

ok, after using Luis's solution below, it seems to validate registration numbers though I am now ended up with multiple error text. I am using Formik with Yup.. the Formik code is as per below ...

<Grid
              container
              item
              lg={12}
              md={12}
              xs={12}
              spacing={15}
              name="vehicles"
              style={{ marginBottom: "-4em" }}
            >
              <Box mx={3} my={2} textAlign="center">
                <h2>Vehicle Details</h2>
              </Box>
            </Grid>
            <Grid
              container
              item
              lg={12}
              md={12}
              xs={12}
              spacing={15}
              name="vehicles"
              style={{ marginBottom: "-4em" }}
            ></Grid>
            {initialValues.vehicles.map((vehicle, index) => (
              <Grid
                container
                item
                lg={10}
                md={12}
                xs={12}
                spacing={5}
                justify="space-between"
                className={classes.rowSpacing}
                key={index}
              >
                <Grid item lg={3} md={5} xs={12}>
                  <Field
                    component={TextField}
                    fullWidth={true}
                    label="Registration Number"
                    name={`vehicles[${index}].registrationNumber`}
                  />
                  <FormHelperText error>
                    <ErrorMessage name="vehicles" />
                  </FormHelperText>
                </Grid>

                <Grid item lg={2} md={5} xs={12}>
                  <Field
                    component={TextField}
                    fullWidth={true}
                    label="Make"
                    name={`vehicles[${index}].make`}
                  />
                </Grid>
                <Grid item lg={2} md={5} xs={12}>
                  <Field
                    component={TextField}
                    fullWidth={true}
                    label="Model"
                    name={`vehicles[${index}].model`}
                  />
                </Grid>
                <br />
              </Grid>
            ))}
          </Grid>

see multiple error below ..error text "at least one registration number is required" getting repeated twice

enter image description here

Hi Luis that updated solution didn't work and it could be due to the fact that I use both Formik-material-ui and material-ui in my forms..see below.. to distinguish between two TextField elements, I am using alias of muiTextField here

import {TextField as muiTextField} from "@material-ui/core";

import { TextField, Select } from "formik-material-ui";

below is my updated code ...

<Grid item lg={3} md={5} xs={12}>
                  <Field
                    //component={TextField}
                    fullWidth={true}
                    label="Registration Number"
                    name={`vehicles[${index}].registrationNumber`}
                    render={() => (
                      <muiTextField
                         error={Boolean(errors.vehicles)}
                         helperText= {
                            errors.vehicles && getVehiclesErrors(errors.vehicles)
                         }
                      />
                   )}
                  />
                  
                </Grid>

The formik-material-ui is just a wrapper around common material-ui elements. I am curious as to why you have 'email' reference in the getVehicleErrors() function. is that a typo? After I updated my code (as per above) here muiTextField refers to the TextField of material-ui, now I see no vehicle registration fields on my form..see below

enter image description here

Managed to fix the issue with below changes....

    const getVehiclesErrors = (errors) => {
    return Array.isArray(errors)
      ? errors.filter((registrationNumber, i, arr) => arr.indexOf(registrationNumber) === i)
      : errors;
  }; 



<Grid item lg={3} md={5} xs={12}>
                      <Field
                        component={TextField}
                        fullWidth={true}
                        label="Registration Number"
                        name={`vehicles[${index}].registrationNumber`}
                      />
                      {errors.vehicles && touched.vehicles ? (
                        <div style={{ color: "red" }}>
                          {getVehiclesErrors(errors.vehicles)}
                        </div>
                      ) : null}
                    </Grid>

enter image description here

Petcock answered 22/8, 2020 at 9:34 Comment(0)
S
5

you can do like this way:

*I've done an example where at least one registrationNumber in array should be filled for your vehicles schema:

vehicles: Yup.array(
    Yup.object({
      registrationNumber: Yup.string(),
      make: Yup.string().required("make Required"),
    }).test(
      "registrationNumber test",
      // The error message that should appears if test failed
      "at least one registrationNumber should be filled",
      // Function that does the custom validation to this array
      validateAgainstPrevious
    )
  )

The function below its just an example. You can do your own logic here.

function validateAgainstPrevious() {
  // In this case, parent is the entire array
  const { parent } = this;
  
  // filtered array vechicles that doens't have registrationNumber
  const filteredArray = parent.filter((e) => !e.registrationNumber);
  
  // If length of vehicles that doesn't have registrationNumber is equals to vehicles  array length then return false;
  if (filteredArray.length === parent.length) return false;

  return true;
}

UPDATED

For the multiple error text issue, you can do a workaround:

Instead you pass TextField component to Field, like this:

<Field
   component={TextField}
   fullWidth={true}
   label="Registration Number"
   name={`vehicles[${index}].registrationNumber`}
/>

you can do this:

<Field
   // component={TextField}
   fullWidth={true}
   label="Registration Number"
   name={`vehicles[${index}].registrationNumber`}
   render={() => (
      <TextField
         error={Boolean(errors.vehicles)}
         helperText= {
            errors.vehicles && getVehiclesErrors(errors.vehicles)
         }
      />
   )}
/>

And created this function:

const getVehiclesErrors = (errors) => {
  return Array.isArray(errors)
    ? errors.filter((email, i, arr) => arr.indexOf(email) === i)
    : errors;
};
Strophe answered 22/8, 2020 at 16:41 Comment(9)
Thanks @Luis for the solution. Your solution does correctly validates the Registration Number though it doesn't display the error message "at least one registrationNumber should be filled" .. I am using Yup with Formik and could see the "Save" button is not enabled till I enter at least one registration number so, the logic is 100% valid.. now it's just the error message part issue.Petcock
Hi @dev, how are you using the formik? Can you update the question with it and how are you setting the errors into form?Strophe
Hi @Luis, actually it was my bad, forgot to add Formik error tags for the registration number though now the the error text appears twice on each validation error.. see my Formik code below... I guess this duplication is due to the way it checks both registration number elements within the vehicles array of JSON objects.. am I right? sorry the code text is too long and it doesn't let me post it here but the added segment is like <FormHelperText error> <ErrorMessage name="vehicles" /> </FormHelperText>Petcock
ok @Luis I've updated the original question with Formik code snippet and a screen image for your reference.Petcock
Hey @Dev, i updated the answer with a workaround to your multiple error text issue.Strophe
Hi @Luis thanks very much for your ongoing help. I've updated the original question with more details around your suggested updated solution. i.e. it didn't work.Petcock
Her @Dev, i think the problem its beacuse you set {TextField as muiTextField} as CamelCase. Try to set it as {TextField as MuiTextField} - PascalCaseStrophe
ok, thanks Luis for the updates.. I've managed to fix it now. please see the updated answer. Thanks tons for all your help. Much appreciated. I asked this same question on Yup git repo and no one bothered to answer. While Yup is a great library for JS validations, I found it needs more comprehensive documentation and should also factor scenarios like this one. It's is very common to optionally validate one or more values within an array of JSON values. Surprised that they don't have it out of box.Petcock
Hey @Dev, no problems. I'm glad I could help you. I agree with you about Yup and lucky for us that stackoverflow exists... :)Strophe
H
2

Instead Yup.object({}) use Yup.object().shape({ ... all the properties comes here})

Hegel answered 5/8, 2021 at 19:17 Comment(0)
P
0

I have done this type of validation in my Node.js(Express.js) project. You can try validation in this way.

const validationSchema = yup.object({
  subject: yup.string().required(),
  description: yup.string().required(), 
  daysOfWeek: yup.array(
    yup.object({
      dayOfWeek: yup.string().required(),
      checked: yup.boolean().required()
    })
  )
})
Prevost answered 6/6, 2023 at 6:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.