Cheat sheet · Laravel 13.37 · Inertia 3
Laravel 13 validation cheat sheet
Where to validate, the typed Rule:: builders that arrived in 13.36, conditional and array rules, database, date, file and password rules, custom rules, and how the errors reach a Vue form with Inertia and Precognition. Every chain was run on Laravel 13.37.
Checked against the official docs on 2026-10-17
Where to validate
In the controller
php
$validated = $request->validate([
'title' => ['bail', 'required', 'string', 'max:255'], // bail: stop this field at the first failure
'body' => ['required'],
]);
$request->validated(); $request->safe()->only(['title']); $request->safe()->except(['body']);
$request->validateWithBag('post', [...]); // named error bagForm Request (php artisan make:request StorePostRequest)
php
public function authorize(): bool { return $this->user()->can('create', Post::class); }
public function rules(): array { return ['title' => [Rule::required(), Rule::string()->max(255)]]; }
public function messages(): array { return ['title.required' => 'Give the post a title.']; }
public function attributes(): array { return ['email' => 'email address']; } // :attribute in messages
public function after(): array { return [fn (Validator $v) => $v->errors()->add('title', 'Nope.')]; }
// Manual: Validator::make($data, $rules)->after(fn ($v) => ...)->stopOnFirstFailure()->validate();Rule:: builders (13.36): typed instead of strings
Autocomplete, and comma-safe values
php
use Illuminate\Validation\Rule;
'account_type' => [Rule::required(), Rule::in(['personal', 'business'])],
'company' => [Rule::missingIf('account_type', 'personal')],
'email' => [Rule::requiredWithout('phone'), Rule::email()],
'phone' => [Rule::requiredWithout('email'), Rule::regex('/^\+?[0-9 ]{7,15}$/')],
'password' => [Rule::required(), Rule::confirmed()->customField('password_repeat')],
'server_ip' => [Rule::nullable(), Rule::ip()->version(4)], // it is ip(), not ipAddress()
'terms' => [Rule::accepted()],
(string) Rule::requiredWith('phone', 'email'); // "required_with:phone,email": strings still work, mix freelyStrings, numbers, booleans, enums
Fluent type rules
php
'title' => [Rule::string()->min(3)->max(255)], // alpha(), alphaDash(), ascii(), startsWith(), lowercase()...
'slug' => [Rule::regex('/^[a-z0-9-]+$/'), Rule::notRegex('/--/')],
'price' => [Rule::numeric(strict: true)->min(0)->decimal(0, 2)], // strict (13.37): "9.5" fails, 9.5 passes
'qty' => [Rule::numeric()->integer()->between(1, 100)], // digits(), maxDigits(), multipleOf(), greaterThan('min')
'tos' => [Rule::boolean(strict: true)], // only true/false, not "1"/"true"
'status' => [Rule::enum(Status::class)->only([Status::Draft, Status::Live])], // except()
'color' => [Rule::hexColor()], 'meta' => [Rule::json()], 'tz' => [Rule::timezone()], 'id' => [Rule::uuid()->version(7)]Conditional rules
Field-based, callback-based, and sometimes
php
'company' => [Rule::requiredIf(fn () => $this->input('account_type') === 'business')], // requiredUnless, prohibitedIf, excludeIf: callback or bool
'vat' => [Rule::presentIf('account_type', 'business'), Rule::nullable()], // missingIf, acceptedIf, declinedIf: field + value
'notes' => [Rule::prohibits('tagline')], 'legacy' => [Rule::excludeWith('new_field')], // excluded from validated()
'plan' => Rule::when($this->user()->isPro(), ['required', 'in:team,business'], ['nullable']), // Rule::unless(...)
'reason' => ['sometimes', 'required', 'max:500'], // only validated when present in the input
$validator->sometimes('reason', ['required', 'max:500'], fn (Fluent $input) => $input->games >= 100);Arrays and nested data
Wildcards, keys, per-item rules
php
'tags' => [Rule::array(), 'max:5'],
'tags.*' => [Rule::string()->max(20), Rule::distinct()->ignoreCase()], // error lands on tags.1
'address' => [Rule::array(['street', 'city'])], // only these keys allowed; requiredArrayKeys()
'options' => [Rule::contains(['a', 'b'])], // doesntContain()
'companies.*.id' => Rule::forEach(fn (?string $value, string $attribute) => [
Rule::exists(Company::class, 'id'), new HasPermission('manage-company', $value),
]),
'username' => [Rule::required(), Rule::anyOf([['string', 'email'], ['string', 'alpha_dash', 'min:6']])],Database, dates, formats
unique, exists, date, email, url
php
'email' => [Rule::unique('users', 'email')->ignore($this->user())->withoutTrashed()], // ignoreModel(), whereNull(), using(fn ($q) => ...)
'team' => [Rule::exists('teams', 'id')->where(fn ($q) => $q->where('owner_id', $this->user()->id))],
'publish_at' => [Rule::date()->format('Y-m-d')->afterToday()], // before(), between($a, $b), todayOrAfter(), equals('2026-10-14')
'email' => [Rule::email()->rfcCompliant(strict: true)->preventSpoofing()->validateMxRecord()],
'site' => [Rule::url()->protocols(['https'])], 'ip' => [Rule::ip()->version(6)], 'code' => [Rule::ulid()],Files and passwords
File::image, File::types, Password
php
use Illuminate\Validation\Rules\{File, Password};
'cover' => [Rule::nullable(), File::image()->min(1024)->max(12 * 1024) // KB; or ->max('12mb')
->dimensions(Rule::dimensions()->maxWidth(2000)->ratio(16 / 9))], // minWidth(), ratioBetween()
'report' => [File::types(['pdf', 'csv'])->max('10mb')], // Rule::imageFile(allowSvg: true)
'password' => [Rule::required(), Rule::confirmed(), Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised()],
'current_password' => [Rule::currentPassword()],
// AppServiceProvider::boot(): Password::defaults(fn () => $this->app->isProduction() ? Password::min(12)->uncompromised() : Password::min(8));
'password' => ['required', Password::defaults()],Custom rules
make:rule, closures, implicit
php
php artisan make:rule Uppercase # --implicit to run on empty values too
class Uppercase implements ValidationRule // DataAwareRule to see other fields, ValidatorAwareRule for the validator
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) $fail('The :attribute must be uppercase.');
}
}
'name' => ['required', new Uppercase, fn (string $attribute, mixed $value, Closure $fail) => $value === 'foo' && $fail("{$attribute} is invalid.")],Errors in Vue with Inertia
useForm, <Form> and Precognition
vue
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3'
const form = useForm({ title: '', email: '' })
const submit = () => form.post('/posts', { onError: (errors) => console.log(errors) }) // 422 → form.errors, no page change
</script>
<template>
<form @submit.prevent="submit">
<input v-model="form.title" @input="form.clearErrors('title')" />
<p v-if="form.errors.title">{{ form.errors.title }}</p> <!-- first message per field -->
<button :disabled="form.processing">Save</button>
</form>
<!-- Live validation with the same rules (Precognition middleware on the route): -->
<Form action="/posts" method="post" #default="{ errors, invalid, validate, validating }">
<input name="title" @change="validate('title')" /> <p v-if="invalid('title')">{{ errors.title }}</p>
</Form>
</template>
<!-- JSON clients get 422 { message, errors: { title: ['...'] } } -->Sources: Laravel docs: Validation · Illuminate\Validation\Rule (13.x source) · Laravel 13.36 rule builders (PR #61833) · Inertia docs: Forms and validation
Free PDF
Get the Validation cheat sheet as a printable PDF
A4, two pages, same content as this page. Join the monthly Laravel & Vue newsletter and the download link appears right here.
You're on the list. Here is your cheat sheet:
Download the PDF