Resources

Validation

Field rules, resource rules, rules that depend on the entry, labels and messages, unknown keys, and how the package validates each payload in one pass.

Keep your API free of database errors: every value that reaches a column should pass a rule first. If a column is not nullable, make its field required on creation. The package validates every payload before any query, and answers a standard Laravel 422 with the exact key of each error.

Field rules

Each field validates its value with three layers:

  1. the base rule of its type: string for Text, integer for Integer, numeric for Number, boolean for Boolean, date for Date and DateTime, the enum for Enum, array for Json, the key type for ID;
  2. nullable, when the field is nullable(). Otherwise null is refused;
  3. the rules you declare: rules() on every write, creationRules() on create, updateRules() on update, sync and toggle.
use Illuminate\Validation\Rule;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Text;

Text::make('title')
    ->rules('max:255')
    ->creationRules('required')
    ->updateRules('filled'),
Integer::make('views')->rules('min:0'),
Text::make('status')->rules(Rule::in(['draft', 'published'])),

Each method takes Laravel rules: strings, pipe strings, rule objects or closures. Repeated calls add up.

{"mutate": [{"operation": "create", "attributes": {"title": 12, "views": "many"}}]}
{
  "message": "The mutate.0.attributes.title field must be a string. (and 1 more error)",
  "errors": {
    "mutate.0.attributes.title": ["The mutate.0.attributes.title field must be a string."],
    "mutate.0.attributes.views": ["The mutate.0.attributes.views field must be an integer."]
  }
}
  • The rules of each field apply at every level of a mutation, under its own key, such as mutate.0.relations.products.1.attributes.price.
  • A rule that names another attribute, such as required_if:featured,true or after_or_equal:published_on, compares with that attribute in the same entry when the resource declares a field of that name.
  • A Rule::when() condition receives the attributes of its entry.
  • The pivot fields of a many-to-many relation validate the pivot of each entry the same way, under mutate.0.relations.tags.0.pivot.weight. See Relationships.

Resource rules

rules(), createRules() and updateRules() on the resource hold the rules that do not belong to one field: rules that compare attributes, and nested keys of a Json field:

public function rules(): array
{
    return [
        'metadata.source' => ['nullable', 'url'],
        'ends_on' => ['after:starts_on'],
    ];
}

public function createRules(): array
{
    return [
        'name' => 'string|min:3',
    ];
}

public function updateRules(): array
{
    return [];
}

Each key gets one list: the field rules, then createRules() on create or updateRules() on update, sync and toggle, then rules(). Pipe strings are split before the merge, so 'required' in createRules() and 'string|min:3' in rules() give ['required', 'string', 'min:3'].

The three methods are read once per request, and every entry of the mutation shares the rule objects they return.

Rules that depend on the entry

A rule that needs the entry it validates, such as a unique rule that ignores the row being updated, is built with Contextual::rule(). The closure receives a Lomkit\Rest\Mutations\MutationContext:

use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Unique;
use Lomkit\Rest\Mutations\Contextual;
use Lomkit\Rest\Mutations\MutationContext;

Text::make('title')->rules(
    Contextual::rule(fn (MutationContext $mutation): Unique => Rule::unique(Post::class, 'title')->ignore($mutation->key)),
),
PropertyHolds
operationthe Lomkit\Rest\Fields\MutationOperation of the entry
keythe key of the entry, null on create and for a key list
depth0 at the root, one more per relation level

The closure returns a rule, a pipe string or a list of rules. A rule object can implement Lomkit\Rest\Mutations\MutationAwareRule instead: the package calls its setMutation(MutationContext $mutation): static on a clone of the rule for each entry.

Contextual::rule() and MutationAwareRule apply to the attributes and the pivot of a mutation. Elsewhere, such as in the rules of an action field, a contextual rule throws: "A contextual rule only applies to the attributes and the pivot of a mutation."

Labels and messages

label() names the key of a field in validation messages. messages() replaces Laravel's text for the rules of that key, keyed by rule name:

Text::make('email')
    ->label(__('email address'))
    ->creationRules('required', 'unique:users,email')
    ->messages(['unique' => __('This :attribute is already registered.')]),

On create, without email, the error is The email address field is required., and with a taken address This email address is already registered., both under mutate.0.attributes.email.

  • The labels and messages apply to the attributes of mutate at every level, to pivot fields, to the fields of actions and instructions, and to the parameters of scopes. The error keys do not change.
  • The package does not translate them: pass a translated string. The declarations are read during the request, so __() follows its locale.
  • A rule object is keyed by its snake class name, such as enum for the rule of an Enum field.
  • A validation.attributes line of your translation files still names a key that has no label.

Package messages

The messages that the package writes itself, such as the totals, the depths, the include aliases, the not-writable text of mutate and the relation texts, are translation lines under the rest namespace: rest::validation, rest::actions and rest::errors. The package ships them in English and French, and answers in the locale of the request. Laravel's own lines, such as validation.prohibited, validation.max.array or validation.exists, still come from your lang/<locale>/validation.php.

With the fr locale, a relation that the resource does not declare gives:

422 Unprocessable Content
{
  "message": "La clé suppliers n'est pas valide pour le champ mutate.0.relations.",
  "errors": {
    "mutate.0.relations": ["La clé suppliers n'est pas valide pour le champ mutate.0.relations."]
  }
}

Publish the files to change a line or add a locale:

php artisan vendor:publish --tag=rest-lang
lang/vendor/rest/fr/validation.php
return [
    'unknown_key' => 'La relation :key est inconnue sous :attribute.',
    // ...
];

The same request then gives La relation suppliers est inconnue sous mutate.0.relations.. A JSON line keyed by the English sentence no longer applies to these messages. See Translations for every file and line.

Unknown keys

Inside an object of a payload, a key that the package does not know gives a 422 on that key, named as written:

{"search": {"filters": [{"field": "price", "operater": "<", "value": 100}]}}
{
  "message": "The search.filters.0.operater field is prohibited.",
  "errors": {
    "search.filters.0.operater": ["The search.filters.0.operater field is prohibited."]
  }
}

This covers the filters, sorts, selects, scopes, aggregates, includes, instructions and text of a search, the entries of the fields of an action or an instruction, and the entries of mutate: The mutate.0.atributes field is prohibited.

A key at the first level is ignored, so that a later release can add keys: a key of the request body, of search, or of the response block of a mutation.

Keys

Every key that a client sends is typed by the ID field of its resource, and checked through the perimeterQuery() of that resource:

  • key in mutate, at the root and under relations;
  • resources on destroy, restore and forceDestroy, and on the actions with the Resources target.

A key of the wrong type gives a type message, such as The mutate.0.key field must be an integer.. A key that the perimeter does not return gives The selected mutate.0.key is invalid., as a key that does not exist. The check runs one query per resource class and per 1,000 keys, and the endpoint writes the rows that this query loaded. See Perimeter.

Lists

Each list of a payload must be a JSON list: filters, sorts, includes, mutate, a to-many relation, a key list, resources, and the fields of an action or an instruction. An object gives a 422 on the list: The search.filters field must be a list.. An entry that is not an object gives The search.sorts.0 field must be an array..

Each list accepts a limited number of entries. See Limits.

One validator per request

Each request turns its payload into Laravel rules on explicit keys, such as search.filters.0.field, fields.0.name and mutate.0.attributes.name, then validates them in one pass with one Laravel validator. Every error of the payload comes back in one 422, nested filters, includes, instructions and relations included.

  • The policy checks of the endpoint and of every resource that the payload reaches run before validation: a denied check answers 403 before any 422. See Policies.
  • Nothing is written before the payload passes. The key checks above read their rows during validation.
  • $request->payload() returns the validated payload as typed nodes once the request passed validation.
  • With Precognition enabled, a request can validate a payload, or one key of it, without running the endpoint. See Precognition.