Skip to content

Schema validation

Demos

Schema for single field

Code Editor
const ajv = makeAjvInstance()
const schema = {
  type: 'string',
  minLength: 5,
}
render(
  <Form.Handler ajvInstance={ajv}>
    <Field.String schema={schema} />
  </Form.Handler>,
)

Schema for a whole data set

Company information

Code Editor
const ajv = makeAjvInstance()
const schema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
      minLength: 2,
    },
    address: {
      type: 'string',
      minLength: 3,
    },
  },
  required: ['name', 'address'],
}
render(
  <Form.Handler
    data={{
      address: 'Prefilled address',
    }}
    schema={schema}
    ajvInstance={ajv}
  >
    <Form.Card bottom="small">
      <Form.MainHeading>Company information</Form.MainHeading>

      <Field.String path="/name" label="Name" />
      <Field.String path="/address" label="Address" />
    </Form.Card>

    <Form.SubmitButton />
  </Form.Handler>,
)

Schema with if-rule

Customer information

Code Editor
const ajv = makeAjvInstance()
const schema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
    },
    customerType: {
      type: 'string',
      enum: ['corporate', 'private'],
    },
    companyName: {
      type: 'string',
    },
  },
  if: {
    properties: {
      customerType: {
        enum: ['corporate'],
      },
    },
    required: ['customerType'],
  },
  then: {
    required: ['name', 'companyName'],
  },
  else: {
    required: ['name'],
  },
}
render(
  <Form.Handler schema={schema} ajvInstance={ajv}>
    <Form.Card>
      <Form.MainHeading>Customer information</Form.MainHeading>

      <Field.String path="/name" label="Name" />
      <Field.String
        path="/customerType"
        label="Customer type (corporate or private)"
      />
      <Field.Name.Company
        path="/companyName"
        labelDescription="Company name (required for corporate customers)"
      />
    </Form.Card>

    <Form.SubmitButton />
  </Form.Handler>,
)

Dependent list schema

Becoming a new customer, this form requires at least one normal account to be added, unless the customer opens a BSU account, then normal accounts can still be added, but is optional.

Customer information

Phone number

Accounts

Standard accounts

BSU Account

Code Editor
const ajv = makeAjvInstance()
const schema = {
  type: 'object',
  definitions: {
    account: {
      type: 'object',
      properties: {
        accountNumber: {
          type: 'string',
          pattern: '^[0-9]{11}$',
        },
        alias: {
          type: 'string',
          minLength: 2,
          maxLength: 32,
        },
      },
      required: ['accountNumber'],
    },
  },
  properties: {
    name: {
      type: 'string',
    },
    email: {
      type: 'string',
    },
    phone: {
      type: 'string',
    },
    accounts: {
      type: 'array',
      items: {
        $ref: '#/definitions/account',
      },
    },
    bsuAccount: {
      $ref: '#/definitions/account',
    },
  },
  oneOf: [
    {
      properties: {
        accounts: {
          type: 'array',
          minItems: 1,
        },
      },
    },
    {
      properties: {
        accounts: {
          type: 'array',
          minItems: 0,
        },
        bsuAccount: {
          type: 'object',
          required: ['accountNumber'],
        },
      },
      required: ['bsuAccount'],
    },
  ],
}
render(
  <Form.Handler
    data={{
      accounts: [{}],
    }}
    schema={schema}
    ajvInstance={ajv}
  >
    <Flex.Vertical>
      <Form.MainHeading>Customer information</Form.MainHeading>
      <Form.Card>
        <Field.String path="/name" label="Name" />
        <Field.Email path="/email" label="E-mail" />
        <Field.PhoneNumber path="/phone" label="Phone number" />
      </Form.Card>

      <Form.MainHeading>Accounts</Form.MainHeading>
      <Form.Card>
        <Form.SubHeading>Standard accounts</Form.SubHeading>

        <Iterate.Array path="/accounts">
          <Flex.Horizontal align="flex-end">
            <Field.BankAccountNumber
              itemPath="/accountNumber"
              label="Account number"
            />
            <Field.String itemPath="/alias" label="Alias" width="medium" />
            <Iterate.RemoveButton />
          </Flex.Horizontal>
        </Iterate.Array>

        <Iterate.PushButton
          text="Add account"
          path="/accounts"
          pushValue={{}}
        />

        <Form.SubHeading>BSU Account</Form.SubHeading>
        <Field.BankAccountNumber
          path="/bsuAccount/accountNumber"
          label="Account number"
        />
        <Field.String path="/bsuAccount/alias" label="Alias" />
      </Form.Card>

      <Form.SubmitButton />
    </Flex.Vertical>
  </Form.Handler>,
)

Dependent schema across sections

This schema validates fields across different sections based on the value of another field.

NB: For schema validation to work with Iterate.Array, the form data must contain an array (empty or populated), otherwise Ajv skips the array schema.

Membership

Beneficial owners

{
  "beneficialOwners": {
    "addedExistingBeneficialOwners": []
  }
} 
{
  "fieldErrors": {},
  "formErrors": {}
} 
Code Editor
const ajv = makeAjvInstance()
const counts = [0, 1, 2, 3]
const schema = {
  type: 'object',
  properties: {
    members: {
      type: 'object',
      properties: {
        numberOfMembers: {
          type: 'integer',
        },
      },
      required: ['numberOfMembers'],
    },
    beneficialOwners: {
      type: 'object',
      properties: {
        addedExistingBeneficialOwners: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              name: {
                type: 'string',
              },
            },
            required: ['name'],
          },
        },
      },
    },
  },
  dependentSchemas: {
    members: {
      allOf: counts.map((count) => ({
        if: {
          properties: {
            members: {
              type: 'object',
              properties: {
                numberOfMembers: {
                  const: count,
                },
              },
              required: ['numberOfMembers'],
            },
          },
        },
        then: {
          required: ['beneficialOwners'],
          properties: {
            beneficialOwners: {
              type: 'object',
              properties: {
                addedExistingBeneficialOwners: {
                  type: 'array',
                  minItems: count,
                  maxItems: count,
                },
              },
              required: ['addedExistingBeneficialOwners'],
            },
          },
        },
      })),
    },
  },
}
render(
  <Form.Handler schema={schema} ajvInstance={ajv}>
    <Flex.Stack>
      <Form.Card>
        <Form.MainHeading>Membership</Form.MainHeading>
        <Field.Number
          path="/members/numberOfMembers"
          label="Number of members (1-3)"
          width="small"
          startWith={-1}
          showStepControls
        />
      </Form.Card>

      <Form.Card>
        <Form.SubHeading>Beneficial owners</Form.SubHeading>
        <Iterate.Array
          path="/beneficialOwners/addedExistingBeneficialOwners"
          errorMessages={{
            minItems: 'You must add {minItems} existing owners.',
          }}
          defaultValue={[]}
          animate
        >
          <Section
            innerSpace={{
              top: 'small',
              bottom: 'small',
            }}
            bottom
            backgroundColor="lavender"
          >
            <Field.String itemPath="/name" label="Owner name {itemNo}" />
            <Iterate.RemoveButton />
          </Section>
        </Iterate.Array>
        <Iterate.PushButton
          path="/beneficialOwners/addedExistingBeneficialOwners"
          pushValue={{}}
          text="Add beneficiary"
        />
      </Form.Card>

      <Form.SubmitButton text="Show errors" />

      <Tools.Log label="Form data" />
      <Tools.Errors label="Errors" />
    </Flex.Stack>
  </Form.Handler>,
)

Dependent schema using Zod

This schema is using Zod for validation, and validates fields across different sections based on the value of another field.

Membership

Beneficial owners

{
  "beneficialOwners": {
    "addedExistingBeneficialOwners": []
  }
} 
{
  "fieldErrors": {},
  "formErrors": {
    "/": "Invalid input: expected object, received undefined"
  }
} 
Code Editor
const ownerSchema = z.object({
  name: z.string().optional(),
})
const schema = z
  .object({
    members: z.object({
      numberOfMembers: z.number().int().min(0).max(3).optional(),
    }),
    beneficialOwners: z.object({
      addedExistingBeneficialOwners: z.array(ownerSchema),
    }),
  })
  .superRefine((value, ctx) => {
    const countValue = value.members.numberOfMembers
    if (countValue === undefined) {
      ctx.addIssue({
        code: 'custom',
        path: ['members', 'numberOfMembers'],
        message: 'Field.errorRequired',
      })
      return // stop further validation
    }

    const count = value.members.numberOfMembers
    const owners = value.beneficialOwners.addedExistingBeneficialOwners
    const diff = owners.length - count
    const path = ['beneficialOwners', 'addedExistingBeneficialOwners']
    if (diff < 0) {
      ctx.addIssue({
        code: 'custom',
        path,
        message: 'IterateArray.errorMinItems',
        messageValues: {
          minItems: count,
        },
      })
    }
    if (diff > 0) {
      ctx.addIssue({
        code: 'custom',
        path,
        message: 'IterateArray.errorMaxItems',
        messageValues: {
          maxItems: count,
        },
      })
    }
  })
render(
  <Form.Handler schema={schema}>
    <Flex.Stack>
      <Form.Card>
        <Form.MainHeading>Membership</Form.MainHeading>
        <Field.Number
          path="/members/numberOfMembers"
          label="Number of members (1-3)"
          width="small"
          startWith={-1}
          showStepControls
        />
      </Form.Card>

      <Form.Card>
        <Form.SubHeading>Beneficial owners</Form.SubHeading>
        <Iterate.Array
          path="/beneficialOwners/addedExistingBeneficialOwners"
          defaultValue={[]}
          animate
        >
          <Section
            innerSpace={{
              top: 'small',
              bottom: 'small',
            }}
            bottom
            backgroundColor="lavender"
          >
            <Field.String
              itemPath="/name"
              label="Owner name {itemNo}"
              required
            />
            <Iterate.RemoveButton />
          </Section>
        </Iterate.Array>
        <Iterate.PushButton
          path="/beneficialOwners/addedExistingBeneficialOwners"
          pushValue={{}}
          text="Add beneficiary"
        />
      </Form.Card>

      <Form.SubmitButton text="Show errors" />

      <Tools.Log label="Form data" />
      <Tools.Errors label="Errors" />
    </Flex.Stack>
  </Form.Handler>,
)