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#
- The reader submits.
validateFormruns over every field that is notgeneratedValue. - Each field's
violationsarray is cleared, then repopulated by its validator. - If any field carries a violation,
onSubmitnever fires and the form re-renders with the messages in place. - Otherwise the form's own
onSubmitruns, then the hook's, thenonSuccesspublishes.
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:
{
"@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#
| Name | Type | Default | Description |
|---|---|---|---|
propertyPath | string | — | Which field it belongs to. An empty string means the form as a whole. |
message | string | — | What is shown under the field. |
code | string | — | Machine-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.