Skip to content

3. Validation

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/create

The 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:

  1. store/update recompiles the validator server-side — never from flags the client sent.
  2. Keys the compiled tree does not contain are pruned before Vine runs.
  3. Vine throws E_VALIDATION_ERROR; toInputErrorsBag folds it into Record<fieldPath, message>, first error per field wins.
  4. The controller flashes it and redirects back.
  5. @adonisjs/inertia shares the bag as the page’s errors prop, and useFormEngine maps each entry onto controller.error for 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, no visible: false anywhere 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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close