react-data-form

Validation & API errors

Client-side validators, and mapping an API Platform 422 back onto fields.

A validator throws#

There is no schema language and no rule registry. A field's validator is a function that receives the value and throws if it is unhappy. The message becomes the violation shown under the field.

inputs: {
  slug: {
    label: "Slug",
    validator: (value) => {
      if (!/^[a-z0-9-]+$/.test(String(value ?? ""))) {
        throw new Error("Lowercase letters, digits and dashes only")
      }
      return value
    },
  },
}

Submit with an invalid slug

Zod, without a dependency on it#

The library does not depend on Zod, but it recognises its errors: any thrown error carrying an issues array is unpacked into one violation per issue, each keeping its code.

import { z } from "zod"

inputs: {
  email: {
    label: "Email",
    // Any error carrying `issues` is read as a Zod error, and each issue
    // becomes one violation — so a schema with three rules shows three messages.
    validator: (value) => z.string().email("That is not an email").parse(value),
  },
}

Any library with the same shape works

The check is structural — Object.hasOwn(error, "issues"). Throw an error with an issues array of { code, message, path } from anywhere and it will be read the same way.

When validation runs#

  1. The reader submits. validateForm runs over every field that is not generatedValue.
  2. Each field's violations array is cleared, then repopulated by its validator.
  3. If any field carries a violation, onSubmit never fires and the form re-renders with the messages in place.
  4. Otherwise the form's own onSubmit runs, then the hook's, then onSuccess publishes.

Not on every keystroke

Validators run on submit, not on change. A field that turns red while the reader is still typing their email address is a worse experience, and the library takes that position for you.

Errors from the API#

Client-side validation is a convenience; the server is the authority. An API Platform backend answers a rejected write with application/problem+json:

422 Unprocessable Entity
{
  "@type": "ConstraintViolationList",
  "title": "An error occurred",
  "violations": [
    { "propertyPath": "email", "message": "This email is already registered." },
    { "propertyPath": "", "message": "The account could not be created." }
  ]
}

addErrorFromViolations maps that body back onto the form: each violation lands on the field named by its propertyPath, and a violation with an empty path becomes a form-level error.

import { addErrorFromViolations } from "react-data-form"

const formContext = useForm({
  form: {
    inputs: { email: { label: "Email" }, name: { label: "Name" } },
    onSubmit: async (data) => {
      try {
        return await api.post("/users", data)
      } catch (error) {
        // `error.data` is the problem+json body above.
        formContext.updateForm(
          addErrorFromViolations(formContext.form, error.data)
        )
        throw error
      }
    },
  },
})

Nothing about the HTTP client is assumed

ApiJsonLdError is declared inside the library purely as a shape. Fetch, Axios or openapi-fetch — only the body matters, so any client works.

The violation shape#

NameTypeDefaultDescription
propertyPathstringWhich field it belongs to. An empty string means the form as a whole.
messagestringWhat is shown under the field.
codestringMachine-readable identifier, kept as-is from Zod or from your API.

createViolation builds one, should you want to attach an error to a field from a controller of your own.

Form-level errors#

Some rules span several fields and belong to none of them. The form takes a validator of its own, and errors holds the messages FormErrors renders above the submit button.

{
  // Runs over the whole payload, for rules that span fields.
  validator: (data) => {
    if (data.endsAt < data.startsAt) throw new Error("The end is before the start")
    return data
  },
}

See also

  • Multi-step forms — validation runs per step, and a failing submit jumps back to the first step holding a violation.
  • Resource views — the create and edit views wire this up for you, including the 422 mapping.