← Back to list

How to validate nested field in Formik Form

“validate is a dependency-free, straightforward way to validate your forms”

Sunny Yao in NEXL Engineering · 2025-10-06 05:57 · 1 claps · 5.5 min read
#formik #validation #nested-forms #react
Open on Medium ↗
Wiki topics: 🌐 · Web Development

How to validate nested field in Formik Form

[validate](https://formik.org/docs/api/formik#validate-values-values--formikerrorsvalues--promiseany) is a dependency-free, straightforward way to validate your forms”

Our sync form

Our sync form

Context

Recently we built a new feature which allows user to sync their Nexl contact list to the contact list in the e-marketing tool they are using.

We need three required fields in the sync form:

  1. Connector Name
  2. eMarketing List Name
  3. Fields to Map (which makes sure we’re syncing the correct information)

The Problem

We use Formik to manage the form, and the first two fields are easy to validate using Yup, however, the Fields to Map seems a bit tricky.

Because Fields to Map is one field in the form, but the value is an array of objects, and each object has a Nexl field and a remote field, also the validation rules are more complicated.

Expected Outcome

For Fields to Map, we need the following validation rules:

  • Mapping must be completed:

  • A field pair must have the same field type, except that any kind of field can be mapped to a text field

“First Name” is a text field, but “Date of Birth” is a date field

“First Name” is a text field, but “Date of Birth” is a date field

“3-Birthdate” and “Date of Birth” are both date field

“3-Birthdate” and “Date of Birth” are both date field

“First Name” is a text field, any other kinds of field can be mapped to it

“First Name” is a text field, any other kinds of field can be mapped to it

  • One field can be only mapped once:

The Solution

It’s difficult to validate such a nested field using Yup, so we’re using Formik’s [validate](https://formik.org/docs/guides/validation#validate) prop.

<Formik> and withFormik() take a prop/option called validate that accepts either a synchronous or asynchronous function. See the API reference *here*.

In this way, we can create validation functions for Fields to Map, and add unit tests to cover the function logics. Also, the function will return Formik errors which can be used to display corresponding error message below the target field easily.

Simplified code of Formik Form and Fields

SyncForm.tsx looks like:

enum FieldTypeEnum {
  Text = "Text",
  Date = "Date",
  Number = "Number",
}

interface FieldInfo {
  id: string;
  type: FieldTypeEnum;
}

interface FieldPair {
  nexlField: FieldInfo | null;
  remoteField: FieldInfo | null;
}

interface ISyncFormValues {
  fieldPairs: FieldPair[];
  eMarketingListName: string;
  connectorId: string;
}

return (
    <Formik
      initialValues={initialValues}
      onSubmit={handleSubmit}
      validate={(values: ISyncFormValues) => validateSyncForm({ values })}
    >
      <Form>
        <ConnectorField />
        <EMarketingListNameField />
        <FieldsPairs />  //Fields to Map
        <button type="submit">Save and Sync</button>
      </Form>
    </Formik>
  );

And then FieldPairs.tsx looks like:

   const emptyFieldPair: FieldPair = {
    nexlField: null,
    remoteField: null,
  };

  const [field, meta, helpers] = useField<FieldPair[]>({
    name: "fieldPairs",
  });

  const fieldErrors = Array.isArray(meta.error) ? meta.error : [];

  return (
    <Box>
      {fieldPairs.map((fieldPair: FieldPair, index) => {
        return (
          <Grid container key={`nexl_field_${index}`}>
            <Grid item>
              <FieldToMapField
                index={index}
                errorMessage={fieldErrors?.[index]?.nexlField}
                ...
              />
            </Grid>
            <Grid item>
              <FieldToMapField
                index={index}
                errorMessage={fieldErrors?.[index]?.remoteField}
                ...
              />
            </Grid>
          </Grid>
        );
      })}
      <Button
        category={ButtonCategory.TertiaryBlue}
        startIcon={<Add />}
        onClick={() => helpers.setValue([...fieldPairs, emptyFieldPair])}
      >
        Add Map Field
      </Button>
    </Box>
  );

Simplified code of validating functions and unit test

validateSyncForm.ts looks like:

export const validateSyncForm = ({
  values,
}: {
  values: ISyncFormValues;
}): object | FormikErrors<ISyncFormValues> => {
  const fieldPairsErrors = validateFieldPairs({
    fieldPairs: values.fieldPairs,
  });
  const noFieldPairsError = fieldPairsErrors.length === 0;
  if (
    !validateEMarketingListNameField(values.eMarketingListNameField) &&
    !validateConnectorField(values.connectorField) &&
    noFieldPairsError
  ) {
    return {}; // return empty object when there's no error
  }

  const validationErrors: FormikErrors<ISyncFormValues> = {};
  if (!!validateEMarketingListNameField(values.eMarketingListNameField)) {
    validationErrors.eMarketingListNameField = validateEMarketingListNameField(
      values.eMarketingListNameField,
    );
  }
  if (!!validateConnectorField(values.connectorField)) {
    validationErrors.connectorField = validateConnectorField(
      values.connectorField,
    );
  }
  if (!noFieldPairsError) {
    validationErrors.fieldPairs = validateFieldPairs({
      fieldPairs: values.fieldPairs,
    });
  }
  return validationErrors;
};

validateEMarketingListNameField.ts and validateConnectorField.ts are both simple, take validateEMarketingListNameField.ts as example:

export const validateEMarketingListNameField = (
  eMarketingListName: string,
): FormikErrors<string | undefined> => {
  return eMarketingListName
    ? undefined
    : "Please enter a list name.";
};

And validateFieldPairs.ts looks like:

export const validateFieldPairs = ({
  fieldPairs,
}: {
  fieldPairs: FieldPair[];
}): FormikErrors<FieldPair>[] => {
  const hasNoFieldPairsToValidate = fieldPairs.length === 0;
  if (hasNoFieldPairsToValidate) {
    return [];
  }
  const fieldPairsErrors = fieldPairs.map((fieldPair) => {
    return {
      nexlField: validateNexlField({
        fieldPairs,
        currentFieldPair: fieldPair,
      }),
      remoteField: validateRemoteField({
        fieldPairs,
        currentFieldPair: fieldPair,
      }),
    };
  });
  const hasFieldPairsError = fieldPairsErrors.some(
    (fieldPairError) =>
      fieldPairError.nexlField !== undefined ||
      fieldPairError.remoteField !== undefined,
  );
  if (!hasFieldPairsError) return [];
  else return fieldPairsErrors;
};

And then validateNexlField.ts looks like:

export const validateNexlField = ({
  currentFieldPair,
  fieldPairs,
}: {
  fieldPairs: FieldPair[];
  currentFieldPair: FieldPair;
}): FormikErrors<string | undefined> => {
  const isIncompleteMapping =
    !!currentFieldPair.remoteField?.id && !currentFieldPair.nexlField?.id;
  if (isIncompleteMapping) {
    return "Incomplete mapping. Please complete the mapping.";
  }

  const isDuplicateField =
    !!currentFieldPair.nexlField?.id &&
    fieldPairs.filter(
      (pair) => pair.nexlField?.id === currentFieldPair.nexlField?.id,
    ).length > 1;
  if (isDuplicateField) {
    return "Duplicate fields. Each field can only be mapped once.";
  }

  return undefined;
};

And validateRemoteField.ts looks like:

export const validateRemoteField = ({
  fieldPairs,
  currentFieldPair,
}: {
  fieldPairs: FieldPair[];
  currentFieldPair: FieldPair;
}): FormikErrors<string | undefined> => {
  const isIncompleteMapping =
    !currentFieldPair.remoteField?.id && !!currentFieldPair.nexlField?.id;
  if (isIncompleteMapping)
    return "Incomplete mapping. Please complete the mapping.";

  const isDuplicateField =
    currentFieldPair.remoteField !== null &&
    fieldPairs.filter(
      (pair) => pair.remoteField?.id === currentFieldPair.remoteField?.id,
    ).length > 1;
  if (isDuplicateField)
    return "Duplicate fields. Each field can only be mapped once.";

  // remote field type should match Nexl field type
  // when remote custom field is a text field, Nexl field can be any type
  if (currentFieldPair.remoteField?.type === FieldTypeEnum.Text)
    return undefined;
  else if (
    currentFieldPair.nexlField?.type !== currentFieldPair.remoteField?.type
  )
    return "Invalid format. Please choose a correct format.";
  return undefined;
};

For unit tests, they should cover all the cases, take the one for validateRemoteField.ts as an example:

describe("validateRemoteField", () => {
  it("show error - Incomplete mapping", () => {
    expect(
      validateRemoteField({
        fieldPairs: [],
        currentFieldPair: {
          remoteField: null,
          nexlField: {
            id: "nexlField",
            type: FieldTypeEnum.Text,
          },
        },
      }),
    ).toBe("Incomplete mapping. Please complete the mapping.");
  });
  it("no error when both fields are null - Incomplete mapping", () => {
    expect(
      validateRemoteField({
        fieldPairs: [],
        currentFieldPair: {
          remoteField: null,
          nexlField: null,
        },
      }),
    ).toBeUndefined();
  });
  it("show error - Duplicate fields", () => {
    expect(
      validateRemoteField({
        fieldPairs: [
          {
            remoteField: {
              id: "remoteField",
              type: FieldTypeEnum.Text,
            },
            nexlField: {
              id: "nexlField",
              type: FieldTypeEnum.Text,
            },
          },
          {
            remoteField: {
              id: "remoteField",
              type: FieldTypeEnum.Text,
            },
            nexlField: null,
          },
        ],
        currentFieldPair: {
          remoteField: {
            id: "remoteField",
            type: FieldTypeEnum.Text,
          },
          nexlField: null,
        },
      }),
    ).toBe("Duplicate fields. Each field can only be mapped once.");
  });
  it("no error when there are no duplicate fields", () => {
    expect(
      validateRemoteField({
        fieldPairs: [
          {
            remoteField: {
              id: "remoteField",
              type: FieldTypeEnum.Text,
            },
            nexlField: null,
          },
        ],
        currentFieldPair: {
          remoteField: {
            id: "anotherRemoteField",
            type: FieldTypeEnum.Text,
          },
          nexlField: null,
        },
      }),
    ).toBeUndefined();
  });
  it("no error when field values are null - Duplicate fields", () => {
    expect(
      validateRemoteField({
        fieldPairs: [
          {
            remoteField: null,
            nexlField: null,
          },
          {
            remoteField: null,
            nexlField: null,
          },
        ],
        currentFieldPair: {
          remoteField: null,
          nexlField: null,
        },
      }),
    ).toBeUndefined();
  });
  it("when remote field is a text field, the Nexl field can be any type", () => {
    expect(
      validateRemoteField({
        fieldPairs: [],
        currentFieldPair: {
          remoteField: {
            id: "remoteField",
            type: FieldTypeEnum.Text,
          },
          nexlField: {
            id: "nexlField",
            type: FieldTypeEnum.Date,
          },
        },
      }),
    ).toBeUndefined();
    expect(
      validateRemoteField({
        fieldPairs: [],
        currentFieldPair: {
          remoteField: {
            id: "remoteField",
            type: FieldTypeEnum.Text,
          },
          nexlField: {
            id: "nexlField",
            type: FieldTypeEnum.Number,
          },
        },
      }),
    ).toBeUndefined();
  });
  it("should show error when the Nexl field and remote field type don't match", () => {
    expect(
      validateRemoteField({
        fieldPairs: [],
        currentFieldPair: {
          remoteField: {
            id: "anotherRemoteField",
            type: FieldTypeEnum.Date,
          },
          nexlField: {
            id: "nexlField",
            type: FieldTypeEnum.Number,
          },
        },
      }),
    ).toBe("Invalid format. Please choose a correct format.");
  });
  it("should not show error when the Nexl field and remote field type match", () => {
    expect(
      validateRemoteField({
        fieldPairs: [],
        currentFieldPair: {
          remoteField: {
            id: "anotherRemoteField",
            type: FieldTypeEnum.Number,
          },
          nexlField: {
            id: "nexlField",
            type: FieldTypeEnum.Number,
          },
        },
      }),
    ).toBeUndefined();
  });
});

Now everything works as expected!

Hope this is helpful :)


메타데이터
post_id
f555d1d7e433
slug
how-to-validate-nested-field-in-formik-form-f555d1d7e433
url
https://medium.com/@sunnyyaoops/how-to-validate-nested-field-in-formik-form-f555d1d7e433
canonical_url
https://medium.com/@sunnyyaoops/how-to-validate-nested-field-in-formik-form-f555d1d7e433
author_url
https://medium.com/@sunnyyaoops
status
ok
fetched_at
2026-07-09 13:13:48