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:
sometimes– gate on key presence. No key in data = skip all rulesnullable– gate on value. Key present, value isnull= pass
// 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:
- Strings and numbers –
string,integer,numeric,decimal,email,url,uuid,regex,min,max,size,gt/gte/lt/lte: Strings and Numbers - Dates –
date,date_format,after,before,timezone,Rule::date(): Dates and Time - Files –
file,image,mimes,extensions,dimensions,File::types(): Files and Images - Arrays and JSON –
array,list,json,in_array,contains, wildcard*: Arrays and JSON - Passwords –
Password::min(),letters(),numbers(),uncompromised(): Passwords
Quick Reference Table
All built-in rules grouped by purpose.
Presence
| Rule | What it does |
|---|---|
required | Field must be present and not empty |
filled | Not empty if present |
present | Key must exist in data |
nullable | Allows null |
sometimes | Validate only if key exists |
missing | Key must not exist |
prohibited | Field must be empty or absent |
Conditional
| Rule | What it does |
|---|---|
required_if | Required when another field has a value |
required_unless | Required unless another field equals |
required_with | Required when other fields are present |
required_without | Required when other fields are absent |
prohibited_if | Banned when another field has a value |
prohibited_unless | Banned unless condition met |
exclude_if | Dropped from validated() when condition |
Value Restrictions
| Rule | What it does |
|---|---|
in:a,b,c | Must be one of the listed values |
not_in:a,b,c | Must not be in the list |
Rule::enum() | Must match a PHP backed enum |
accepted | yes/on/true/1 |
declined | no/off/false/0 |
boolean | Castable to boolean |
Rule::anyOf() | Passes at least one rule set |
Comparison
| Rule | What it does |
|---|---|
confirmed | Matches {field}_confirmation |
same:field | Matches the named field |
different:field | Differs from the named field |
gt:field | Greater than |
gte:field | Greater than or equal |
lt:field | Less than |
lte:field | Less than or equal |
Size and Range
| Rule | What it does |
|---|---|
min:n | Minimum (chars, value, items, or KB depending on type) |
max:n | Maximum |
size:n | Exact match |
between:min,max | Inclusive range |
digits:n | Exact digit count |
digits_between:min,max | Digit range (legacy – prefer min_digits/max_digits) |
min_digits:n | Minimum digits |
max_digits:n | Maximum digits |
String Rules
| Rule | What it does |
|---|---|
alpha | Letters only (Unicode by default, ascii for a-z) |
alpha_dash | Letters, digits, dash, underscore |
alpha_num | Letters and digits |
email | Email format |
url | URL format |
uuid | UUID format |
regex:pattern | Matches regular expression |
starts_with:a,b | Starts with one of the values |
ends_with:a,b | Ends with one of the values |
lowercase | All lowercase |
uppercase | All uppercase |
Type
| Rule | What it does |
|---|---|
string | Must be a string |
integer | Must be an integer |
numeric | Must be numeric (including float) |
array | Must be a PHP array |
list | Array with consecutive integer keys |
json | Valid JSON string |
file | Uploaded file |
date | Valid date |
Flow Control
| Rule | What it does |
|---|---|
bail | Stop this field’s chain on first error |
exclude | Drop 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.