Skip to content
AzuAzu

Validation

Create and edit forms submit through the validators named on the resource. Your API’s rules and the admin’s rules are the same rules.

app/admin/resources/product_resource.ts (part)
validators: {
create: createProductValidator,
update: updateProductValidator,
// The update validator's unique rule reads productId
meta: ({ record }) => ({ productId: record?.id }),
},

Azu passes only the form’s declared fields to the validator. So mass assignment stays limited to what you listed.

A resource with no validator for an operation can’t save, by design. Azu never writes input that hasn’t been through one of your validators. Without an update validator, the edit form uses create.

Note: validators written for your API don’t always fit a form. If update needs different rules from create, an optional password say, give the resource a separate update validator.

  • A belongsTo field submits its foreign key: category becomes categoryId.
  • A manyToMany field submits an array of keys: tags becomes tagIds.

Relationships shows the validator rule and how the pivot rows are synced.

Validation errors show under the field that caused them, including nested and array fields like tags.0.

They read as your validator words them. VineJS’s defaults name a field by its key: “The price field must be at least 0”. To use the form’s labels, give the validator a SimpleMessagesProvider with them. Key them by input name: categoryId, not category.

The demo does this with a small helper. First, its messages:

app/validators/messages.ts
export const messages = {
'required': '{{ field }} is required',
'string': '{{ field }} must be text',
'number': '{{ field }} must be a number',
'boolean': '{{ field }} must be yes or no',
'date': '{{ field }} must be a date',
'minLength': '{{ field }} must be at least {{ min }} characters',
'maxLength': '{{ field }} must be {{ max }} characters or fewer',
'min': '{{ field }} must be at least {{ min }}',
'max': '{{ field }} must be {{ max }} or less',
'withoutDecimals': '{{ field }} must be a whole number',
'regex': '{{ field }} is not in the right format',
'enum': '{{ field }} must be one of the options',
'array.maxLength': '{{ field }} can have {{ max }} at most',
'database.unique': '{{ field }} is already taken',
'database.exists': '{{ field }} was not found',
}

Then a helper that gives a validator those messages and its labels:

app/validators/messages.ts
export function labelled<Validator extends { messagesProvider: unknown }>(
validator: Validator,
fields: Record<string, string>,
overrides: Record<string, string> = {}
): Validator {
validator.messagesProvider = new SimpleMessagesProvider({ ...messages, ...overrides }, fields)
return validator
}

A failed min rule on price then reads “Price must be at least 0”. The same file has a minDecimal rule for decimals kept as strings. With it, “-5” fails its bound, not its format.

Some validators need context. An update validator’s unique rule has to skip the record being edited, for example.

Azu passes { record } as VineJS metadata: the record on update, and undefined on create. If your validator expects a different shape, map it with meta. It receives the record and the HTTP context. The demo’s product resource maps it to productId, as shown at the top of this page. Its update validator reads it:

app/validators/product.ts
export const updateProductValidator = labelled(
vine.create({
...fields,
sku: vine
.string()
.trim()
.toUpperCase()
.regex(/^[A-Z0-9-]+$/)
.maxLength(40)
.unique({
table: 'products',
column: 'sku',
filter: (query, _value, field) => {
query.whereNot('id', field.meta.productId)
},
}),
}),
labels,
skuFormat
)