Validation
Validation rules live in the model's schema. The REST API enforces them on every POST and PUT, and answers with 422 when a request breaks one.
Required and optional
Every field is required by default. To make one optional, pass a rule as the last argument. The rule is a function that receives the underlying Zod type and returns the type you want:
schema: z.object({
title: text({ label: "Title" }),
subtitle: text({ label: "Subtitle" }, (z) => z.optional()),
}),Chained rules
The same callback is where you add Zod rules. It receives the Zod type of the field, such as a string for text(), so its methods are available directly:
title: text({ label: "Title" }, (z) => z.min(3).max(100)),
price: numeric({ label: "Price" }, (z) => z.min(0)),
images: array({ label: "Images" }, text({ label: "URL" }), (z) => z.min(1)),Defaults
Use z.default() to give a field a value when a request leaves it out:
featured: boolean({ label: "Featured?" }, (z) => z.default(false)),Defaults are applied by the API. A record you create with model.create() in your own code does not get them.
Validation errors
When a request breaks a rule, the API answers 422 with one entry per invalid field:
{
"message": "Validation error",
"errors": [
{ "path": "title", "message": "Too small: expected string to have >=3 characters" },
{ "path": "status", "message": "Invalid option: expected one of \"DRAFT\"|\"PUBLISH\"" }
]
}Unique fields
Add unique: true to a field's meta to reject duplicates:
slug: text({ label: "Slug", unique: true }),A request that would create a second record with the same value, or change one to a value that is taken, is rejected with 409:
{ "message": "Field \"slug\" must be unique — value already exists", "field": "slug" }Things to know:
- The check runs in njin before the record is written. It is not a database index, so two requests that arrive at the same moment could both pass. This is a deliberate trade-off.
- It also runs for
model.create()andmodel.update()calls in your own code, which throwUniqueConstraintError(exported from@njinlabs/njin). - On a model with
i18n: true, a localized field is unique within each language. - On a draftable model, uniqueness is enforced on the working copy.
Validating data in your own code
model.create() and model.update() do not run the schema's rules. To validate data from an outside source, parse it with the model's schema first:
const data = post.validation.parse(input); // throws a ZodError when invalid
await post.create(data);