Laravel 13 Validation Rules: Complete Reference

Laravel ships with dozens of validation rules, and telling similar ones apart takes some practice. This reference covers the rules you will reach for most often, with practical examples and head-to-head comparisons for the pairs that cause the most confusion. Rules that have a dedicated deep-dive article are described briefly with a link.

Three Ways to Write Rules

Before covering individual rules, a quick note on syntax. Laravel accepts rules in three formats, and picking the right one makes your code easier to read.

Pipe-delimited string – the shortest form:

$request->validate([
    'title' => 'required|string|max:200',
    'category_id' => 'required|integer|exists:categories,id',
]);

Array of rules – required when a rule contains commas or uses a Rule object:

use Illuminate\Validation\Rule;

$request->validate([
    'email' => ['required', 'email:rfc,dns', Rule::unique('users')->ignore($user->id)],
    'role' => ['required', Rule::in(['editor', 'viewer'])],
]);

Fluent builder via the Rule class (Laravel 11+):

$request->validate([
    'username' => [
        'required',
        Rule::string()->min(3)->max(30)->alphaDash(ascii: true),
    ],
    'start_date' => [
        'required',
        Rule::date()->afterToday(),
    ],
]);

All three mix freely in a single validate() call. Use strings for simple cases, arrays when you need Rule objects, and the fluent builder when conditional logic via when() makes sense.

Presence and Requirement

This group determines whether a field must exist and whether it can be empty. Mixing them up is the single most common validation mistake.

required

The field must be in the request and not empty. Empty means: null, empty string "", empty array [], empty Countable, or an uploaded file with no path.

$request->validate([
    'name' => 'required|string|max:100',
    'items' => 'required|array|min:1',
]);

filled

If the field is present in the request, it must not be empty. If the field is absent entirely, validation passes. Useful for APIs where a client may omit a field, but sending an empty value is an error:

$request->validate([
    'nickname' => 'filled|string|max:50',
]);

// {} – passes (field absent)
// {"nickname": "alex"} – passes
// {"nickname": ""} – fails

present vs required

present demands the key exists in the data, but allows any value including null and empty string. required demands the key exists AND is not empty.

$request->validate([
    'comment' => 'present|nullable|string',
]);

// {"comment": null} – passes (key present, null allowed)
// {"comment": ""} – passes
// {} – fails (key missing)

Use present for API contracts where the frontend must explicitly send a key even when the value is empty. Conditional variants exist: present_if, present_unless, present_with, present_with_all.

nullable

Allows null as a valid value. Without it, null will fail any type rule like string or integer.

$request->validate([
    'middle_name' => 'nullable|string|max:50',
    'deleted_at' => 'nullable|date',
    'tags' => 'nullable|array',
    'tags.*' => 'string|max:50',
]);

There is no built-in nullable_if rule. To make a field nullable conditionally, use Rule::when:

'field' => [Rule::when($isOptional, 'nullable'), 'string', 'max:100'],

A common misconception: nullable does not make a field optional. If required comes before nullable, the field must be in the request – but it can arrive as null.

When the value is null and nullable is present, further rules in the chain are skipped. This is important – nullable|email:rfc|unique:users will not hit the database if the value is null.

sometimes

Tells the validator: only check the remaining rules if the field actually exists in the data. If the key is absent, skip the field entirely.

$validator = Validator::make($data, [
    'phone' => 'sometimes|required|string|min:10',
]);

If phone is not in $data, no rules fire. If it is present, required kicks in and an empty string fails.

A frequent problem: sometimes appears to “not work” when ConvertEmptyStringsToNull middleware converts "" to null. The key is still present (just null), so sometimes does not skip it:

// User submits {"phone": ""} → middleware converts to {"phone": null}
// sometimes sees the key "phone" → proceeds to validate
// "required" fails on null → error, even though the field "looks" empty

'phone' => 'sometimes|required|string|min:10', // fails on ""

// Fix: add nullable if empty values should pass
'phone' => 'sometimes|nullable|string|min:10', // "" passes (converted to null, nullable stops chain)

sometimes vs nullable – side by side

This is the most-asked question about Laravel validation. The difference is fundamental:

// PATCH endpoint: field can be omitted. If sent, validate it
'bio' => 'sometimes|nullable|string|max:1000',

// Field is required in request, but value can be null
'supervisor_id' => 'required|nullable|integer|exists:users,id',

The first pattern (sometimes|nullable) is standard for PATCH requests where the client sends only changed fields. The second is for fields like “supervisor” that must always be submitted but legitimately have no value.

A common question: does required|nullable cancel itself out? No. required ensures the key is in the request. nullable allows the value to be null. Together they mean “the key must exist, but null is a valid value.” Without nullable, sending {"supervisor_id": null} would fail the integer check.

Optional Fields

Laravel has no dedicated optional rule. The combination sometimes|nullable achieves the same effect. The ConvertEmptyStringsToNull middleware (enabled by default) turns empty strings into null, which is why nullable usually appears alongside sometimes:

$request->validate([
    'website' => 'sometimes|nullable|url',
    'notes' => 'sometimes|nullable|string|max:2000',
]);

Fields not listed in the rules at all are not validated and do not appear in validated(). If you need a field in the output but it is optional, list it explicitly with sometimes|nullable.

There is no built-in way to set a default value during validation. If a nullable field should have a fallback, handle it after validation:

$validated = $request->validated();
$role = $validated['role'] ?? 'viewer';

Or use prepareForValidation() in a Form Request to inject defaults before rules run.

missing

The opposite of present – the key must not exist in the data. Useful for API versioning or deprecating fields:

$request->validate([
    'legacy_field' => 'missing',
]);

Conditional variants: missing_if, missing_unless, missing_with, missing_with_all.

A related concept: exclude removes a field from the validated() output without failing. exclude_if and exclude_unless do this conditionally. These do not reject the request – they silently drop the field from results.

How Rules Interact

Rules run left to right, and some short-circuit the chain.

nullable stops the chain if the value is null. Everything after it is skipped, and the field passes:

// null passes – email:rfc never runs, no DB query for unique
'contact_email' => 'nullable|email:rfc|unique:users',

A type rule (string, integer, array) determines how min, max, size, and between behave. Without it, Laravel guesses the type from the input, which leads to surprises:

// min:3 checks string length (>= 3 characters)
'name' => 'required|string|min:3',

// min:3 checks numeric value (>= 3)
'quantity' => 'required|integer|min:3',

// no type rule – if "10" arrives, min:3 checks string length of "10" (2 chars – fails!)
'ambiguous' => 'required|min:3',

Always specify a type rule before size-related rules.

bail – Stopping on First Error

By default, Laravel runs every rule for every field and collects all errors. bail changes this: the first failure on that field stops its chain.

$request->validate([
    'email' => 'bail|required|email:rfc|unique:users',
    'password' => 'required|min:8',
]);

If email fails required, the email:rfc and unique:users rules are skipped. The password field is validated independently.

Put bail first for readability. Its main practical benefit: preventing expensive rules (unique, exists) from running when the field already failed a cheaper check.

When bail hurts: if the form should display all errors at once (standard UX), bail forces users to fix issues one by one. Use it selectively for fields with database rules, not everywhere.

For global first-failure behavior across all fields:

if ($validator->stopOnFirstFailure()->fails()) {
    // only the first error of the first failing field
}

Restricting Allowed Values

in and not_in

in checks that the value matches one of the listed options. This is the go-to rule for dropdown fields and status selectors:

$request->validate([
    'status' => 'required|in:draft,published,archived',
    'priority' => ['required', Rule::in([1, 2, 3, 4, 5])],
]);

Type coercion matters. The string syntax in:1,2,3 compares as strings. Rule::in([1, 2, 3]) still uses loose comparison. If you need strict typing, add integer or numeric before in – by the time in runs, the value is guaranteed to be the correct type.

not_in rejects values in the list:

$request->validate([
    'username' => ['required', 'string', Rule::notIn(['admin', 'root', 'system'])],
]);

enum

For PHP backed enums, Rule::enum validates that the input matches one of the enum’s values:

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';
}

$request->validate([
    'status' => ['required', Rule::enum(OrderStatus::class)],
]);

Filter allowed values with only and except:

Rule::enum(OrderStatus::class)->only([
    OrderStatus::Pending,
    OrderStatus::Cancelled,
]);

Rule::enum(OrderStatus::class)->except([OrderStatus::Cancelled]);

Conditional filtering with when:

Rule::enum(OrderStatus::class)->when(
    $user->isManager(),
    fn ($rule) => $rule->only([OrderStatus::Shipped, OrderStatus::Cancelled]),
    fn ($rule) => $rule->except([OrderStatus::Shipped]),
);

When to use in vs enum: if your project has a backed PHP enum, use Rule::enum. If the values live in a config file or database, use Rule::in.

accepted and declined

accepted checks for: "yes", "on", 1, "1", true, "true". Built for terms-of-service checkboxes:

$request->validate([
    'terms' => 'accepted',
    'privacy_policy' => 'accepted',
]);

declined is the inverse: "no", "off", 0, "0", false, "false". Both have conditional forms: accepted_if:field,value and declined_if:field,value.

boolean

Validates that the value can be cast to boolean: true, false, 1, 0, "1", "0". The strict parameter (documented since Laravel 11) only accepts true and false – no string or integer equivalents:

$request->validate([
    'is_active' => 'boolean',
    'notifications' => 'boolean:strict',
]);

Boolean “not working” is almost always caused by the value arriving as a string "true" or "false" from a JSON request. Without strict, string "1" and "0" pass, but "true" as a string does not – it must be the actual boolean true. Check your request payload format.

A nullable boolean is valid: 'is_featured' => 'nullable|boolean' accepts true, false, or null.

If a boolean field must be specifically true (not just truthy), use accepted instead of boolean. There is no built-in “boolean only true” rule, but accepted covers the common case – terms checkboxes, opt-in flags.

More on boolean validation in the strings and numbers article.

Checking Exact Values

Laravel has no equals rule. To check for a specific value, use in with a single option or size for integers:

$request->validate([
    'plan' => ['required', Rule::in(['premium'])],
    // for integers, size checks the value, not length
    'percentage' => 'required|integer|size:100',
]);

With integer, size:10 means “value equals 10”. Without a type rule, size checks string length or array count – a common source of bugs.

Field Comparison

confirmed

Expects a second field with the _confirmation suffix. The classic use: confirm password:

$request->validate([
    'password' => 'required|min:8|confirmed',
]);
// request must contain password_confirmation

Custom confirmation field name:

$request->validate([
    'email' => 'required|email|confirmed:email_repeat',
]);

same and different

same compares two explicitly named fields. different is the opposite:

$request->validate([
    'new_password' => 'required|min:8|different:current_password',
    'new_password_confirmation' => 'required|same:new_password',
]);

The difference between same and confirmed:

// confirmed: rule goes on the source field, suffix is automatic
'password' => 'required|confirmed',
// Laravel looks for password_confirmation

// same: rule goes on the second field, source name is explicit
'password_repeat' => 'required|same:password',

Use confirmed for standard forms with _confirmation suffix. Use same when field names are arbitrary.

Arrays and distinct

distinct prevents duplicate values in array elements. A real-world scenario – a form where users add multiple email recipients:

$request->validate([
    'recipients' => 'required|array|min:1|max:20',
    'recipients.*.email' => 'required|email|distinct',
    'recipients.*.role' => 'required|in:to,cc,bcc',
]);

Without distinct, the same email could appear twice and receive duplicate messages.

By default, distinct uses loose comparison. strict enables type-strict checking, ignore_case ignores letter case:

'options.*.id' => 'distinct:strict',
'categories.*' => 'distinct:ignore_case',

Full array and JSON validation – array, list, in_array, contains, required_array_keys, wildcard * – is covered in the arrays and JSON article.

between – Range Validation

between checks that a value falls within an inclusive range. Its behavior depends on the field type:

$request->validate([
    'age' => 'required|integer|between:18,120',
    'title' => 'required|string|between:5,200',
    'attachments' => 'array|between:1,5',
    'avatar' => 'file|between:10,2048',
]);

For numbers – compares the value. For strings – character count. For arrays – element count. For files – size in kilobytes. This polymorphic behavior is shared by min, max, and size. More in the strings and numbers article.

prohibited and Missing Values

prohibited requires the field to be empty or absent. Empty means: null, empty string, empty array (or empty Countable), file with no path.

Conditional variants are more common in practice. prohibited_if bans a field when another field has a specific value:

$request->validate([
    'is_company' => 'required|boolean',
    'company_name' => 'prohibited_if:is_company,false',
    'personal_id' => 'prohibited_if:is_company,true',
]);

prohibits bans other fields when the current one is present:

$request->validate([
    // when custom_message is provided, template_id is forbidden
    'custom_message' => 'sometimes|string|max:500|prohibits:template_id',
    'template_id'    => 'required_without:custom_message|integer|exists:templates,id',
]);

Heads up: prohibits uses the same “empty” notion as required (null, "", [], file with an empty path). A boolean false is not in that list, so 'flag' => 'sometimes|boolean|prohibits:other' also bans other when the request sends flag: false, which is rarely the intent. For boolean toggles, reach for prohibited_if:flag,true or the checkbox-friendly variants below.

For checkboxes, prohibited_if_accepted and prohibited_if_declined are more expressive than listing values manually:

$request->validate([
    'use_default_address' => 'required|boolean',
    'custom_address' => 'prohibited_if_accepted:use_default_address|string|max:500',
]);

For complex logic, use a closure:

$request->validate([
    'discount_code' => Rule::prohibitedIf(fn () => $order->isPaid()),
]);

Conditional Rules

Rules like required_if, required_with, and required_without make a field required based on other fields:

$request->validate([
    'delivery_type' => 'required|in:pickup,courier',
    'address' => 'required_if:delivery_type,courier|string|max:500',
    'pickup_time' => 'required_if:delivery_type,pickup|date',
]);

required_with makes a field required when any of the listed fields are present:

$request->validate([
    'latitude' => 'required_with:longitude|numeric|between:-90,90',
    'longitude' => 'required_with:latitude|numeric|between:-180,180',
]);

The full set – required_if, required_unless, required_with, required_with_all, required_without, required_without_all, plus the $validator->sometimes() method – is covered in the conditional validation article.

anyOf – OR Logic

Rule::anyOf lets you specify alternative rule sets. The field passes if it satisfies at least one:

$request->validate([
    'contact' => [
        'required',
        Rule::anyOf([
            ['email'],
            ['string', 'regex:/^\+\d{10,15}$/'],
        ]),
    ],
]);

Here contact can be either an email address or a phone number. Without anyOf you would need a custom rule.

Strings and regex

alpha validates Unicode letters by default. With ascii, only a-z and A-Z pass:

$request->validate([
    'first_name' => 'required|alpha',
    'username' => 'required|alpha_dash:ascii',
    'code' => 'required|alpha_num|size:6',
]);

The alpha family does not allow spaces. For names like “Jane Doe”, use regex or just string with a length limit.

regex with a pattern containing | breaks in string syntax because the pipe is parsed as a rule separator:

// Broken – Laravel sees "regex:/^(foo" and "bar)$/" as separate rules
'code' => 'required|regex:/^(foo|bar)$/',

// Correct – array syntax isolates the regex
'code' => ['required', 'regex:/^(foo|bar)$/'],

Full string and number rules in the strings and numbers article.

Database Rules

unique and exists run SQL queries. Both support fluent syntax with filters, soft deletes, and Eloquent model references:

$request->validate([
    'email' => ['required', 'email', Rule::unique('users')->ignore($user->id)],
    'category_id' => ['required', Rule::exists('categories', 'id')->where('active', true)],
]);

Detailed coverage in the unique and exists article.

Rules by Data Type

Each data type has its own deep-dive article:

Quick Reference Table

All built-in rules grouped by purpose.

Presence

RuleWhat it does
requiredField must be present and not empty
filledNot empty if present
presentKey must exist in data
nullableAllows null
sometimesValidate only if key exists
missingKey must not exist
prohibitedField must be empty or absent

Conditional

RuleWhat it does
required_ifRequired when another field has a value
required_unlessRequired unless another field equals
required_withRequired when other fields are present
required_withoutRequired when other fields are absent
prohibited_ifBanned when another field has a value
prohibited_unlessBanned unless condition met
exclude_ifDropped from validated() when condition

Value Restrictions

RuleWhat it does
in:a,b,cMust be one of the listed values
not_in:a,b,cMust not be in the list
Rule::enum()Must match a PHP backed enum
acceptedyes/on/true/1
declinedno/off/false/0
booleanCastable to boolean
Rule::anyOf()Passes at least one rule set

Comparison

RuleWhat it does
confirmedMatches {field}_confirmation
same:fieldMatches the named field
different:fieldDiffers from the named field
gt:fieldGreater than
gte:fieldGreater than or equal
lt:fieldLess than
lte:fieldLess than or equal

Size and Range

RuleWhat it does
min:nMinimum (chars, value, items, or KB depending on type)
max:nMaximum
size:nExact match
between:min,maxInclusive range
digits:nExact digit count
digits_between:min,maxDigit range (legacy – prefer min_digits/max_digits)
min_digits:nMinimum digits
max_digits:nMaximum digits

String Rules

RuleWhat it does
alphaLetters only (Unicode by default, ascii for a-z)
alpha_dashLetters, digits, dash, underscore
alpha_numLetters and digits
emailEmail format
urlURL format
uuidUUID format
regex:patternMatches regular expression
starts_with:a,bStarts with one of the values
ends_with:a,bEnds with one of the values
lowercaseAll lowercase
uppercaseAll uppercase

Type

RuleWhat it does
stringMust be a string
integerMust be an integer
numericMust be numeric (including float)
arrayMust be a PHP array
listArray with consecutive integer keys
jsonValid JSON string
fileUploaded file
dateValid date

Flow Control

RuleWhat it does
bailStop this field’s chain on first error
excludeDrop from validated() output

Practical Recipes

For PATCH endpoints where the client sends only changed fields:

public function rules(): array
{
    return [
        'name' => 'sometimes|required|string|max:100',
        'email' => ['sometimes', 'required', 'email', Rule::unique('users')->ignore($this->user()->id)],
        'bio' => 'sometimes|nullable|string|max:500',
    ];
}

For entity-type forms where fields depend on the selected type:

$request->validate([
    'type' => 'required|in:individual,company',
    'tax_id' => 'required_if:type,company|prohibited_if:type,individual|string|size:10',
    'passport' => 'required_if:type,individual|prohibited_if:type,company|string',
]);

Place bail before rules that hit the database:

$request->validate([
    'email' => 'bail|required|email:rfc|unique:users',
    'invite_code' => 'bail|required|string|exists:invites,code',
]);

When rules outgrow the controller, move them into a Form Request. When built-in rules are not enough, build custom ones. Error message customization is in the error messages article, and the quick start guide covers the fundamentals.