Nothing in chapter 2 declared a validator. required(), maxLength(200) and
nullable() on the fields are the validator: the server compiles a VineJS
schema out of the same tree it rendered, for the exact mode and visibility
state of the submission.
What a submission does
Post the create form with an empty title and you are redirected back with an error under the field:
curl -i -X POST http://localhost:3333/admin/posts \
-d 'title=&slug=no-title&body=x&authorId=1&status=draft'HTTP/1.1 302 Found
location: /admin/posts/createThe redirect carries a session flash named inputErrorsBag:
{ "title": "The title field must be defined" }That name is v7’s, not Adonia’s. The chain is:
store/updaterecompiles the validator server-side — never from flags the client sent.- Keys the compiled tree does not contain are pruned before Vine runs.
- Vine throws
E_VALIDATION_ERROR;toInputErrorsBagfolds it intoRecord<fieldPath, message>, first error per field wins. - The controller flashes it and redirects back.
@adonisjs/inertiashares the bag as the page’serrorsprop, anduseFormEnginemaps each entry ontocontroller.errorfor that field.
A failed login flashes the identical bag from the panel’s auth controller, so “the form failed” has exactly one shape in the client.
The rules you get
| Declaration | Effect |
|---|---|
required() |
The <field> field must be defined |
nullable() |
null accepted; absent is still absent |
maxLength(n) / minLength(n) |
Length bounds on textual fields |
unique() |
Records the constraint (see below) |
rules(v => …) |
Replaces the field type’s Vine base entirely |
F.belongsTo(…) |
Existence: the key must name an in-scope row |
A relation field validates by existence, scoped by the target resource’s
own static query(), so a key the picker would never offer is a key the form
cannot accept:
curl -X POST http://localhost:3333/admin/posts -d 'title=Ghost&…&authorId=99999'{ "authorId": "The selected Author does not exist" }::: warning unique() is declaration-only in this build
The spec is recorded on the field and the DSL is frozen, but nothing reads it
yet: no database.unique rule is emitted and no probe runs. Keep declaring it
— and keep the unique index on the column, which is what actually guarantees
the constraint today. See validation.
:::
Mass assignment is impossible by construction
The fill set — the keys written to the model — is derived from the compiled schema for this submission, not from a list you maintain. A key that is not in the tree is not in the fill set:
await client.post('/admin/posts').form({
title: 'Guard probe',
slug: 'guard-probe',
body: 'probe body',
authorId: String(admin.id),
status: 'draft',
id: '424242', // not a field
author_id: '99999', // the COLUMN, not the field key
createdAt: '1999-01-01', // a timestamp the model owns
admin_flag: 'true', // not even a column
})The row is created with none of the last four applied. That test lives in
examples/blog-admin/tests/functional/adonia_posts.spec.ts and it is not
decorative: the same mechanism is what makes a field hidden by a canSee gate
or a reactive rule unfillable, which chapters 5 and 7 lean on.
Three consequences worth internalising:
- Omission is total. A node the request may not see is absent from the
descriptor, from
state, from the validator and from the fill set — one pass, novisible: falseanywhere on the wire. readonlyOn('edit')keeps a field rendered and validated but out of the fill set for that mode. Chapter 7 uses it to pin a slug.- Client rules are cosmetic. Every declarative rule that affects requiredness is mirrored in the compiled validator, so bypassing the UI gains nothing.
Testing it without HTTP
@adonia/core/testing compiles a resource in-process, which is the cheap way
to pin a rule:
import { validateWith } from '@adonia/core/testing'
const { outcome, errors } = await validateWith(resource, 'create', { slug: 'x' })
// outcome === 'invalid'
// errors === { title: 'The title field must be defined', … }validateWith runs the same prune → Vine → sets pipeline the controller
runs, and hands back errors already in inputErrorsBag shape. The full harness
— compileFor, assertValidatorKeys, assertFillOmits, assertRequiredWhen
— is in testing.
Next: the index table.