Conditional Validation in Laravel: required_if, when, exclude
Not every field matters on every request. A billing address is irrelevant when the user selects free shipping. A company tax ID only applies to business accounts. A doctor’s name field should not even appear in the validated output when has_appointment is false. Laravel handles all of this through conditional validation rules that make fields required, optional, or excluded based on the value of another field.
required_if – Require a Field Based on Another Field’s Value
The most common conditional validation rule. A field becomes required if another field value matches what you specify:
$request->validate([
'payment_type' => 'required|in:cc,bank,paypal',
'credit_card_number' => 'required_if:payment_type,cc|digits:16',
'bank_account' => 'required_if:payment_type,bank|string',
'paypal_email' => 'required_if:payment_type,paypal|email',
]);
When payment_type is cc, the credit_card_number field must be present and not empty. The other two payment fields are not required. If payment_type is bank, only bank_account is required, and so on.
You can match against multiple values by listing them after the field name:
$request->validate([
'role' => 'required|in:admin,editor,viewer',
'team_id' => 'required_if:role,admin,editor|exists:teams,id',
]);
Here team_id is required if role is either admin or editor, but not for viewer.
Rule::requiredIf for Complex Conditions
When the condition goes beyond a simple value comparison – checking a database flag, combining multiple fields, or running business logic – use Rule::requiredIf with a closure:
use Illuminate\Validation\Rule;
$request->validate([
'tax_id' => [
Rule::requiredIf(fn () => 'business' === $request->input('account_type')
&& 'EU' === $request->input('region')),
'string',
'max:20',
],
]);
The closure returns true when the field should be required, false otherwise. This handles the “required if multiple conditions” case that required_if alone cannot express – the string syntax only checks one field at a time.
You can also pass a boolean directly without a closure:
$request->validate([
'supervisor_id' => [
Rule::requiredIf($user->is_intern),
'exists:users,id',
],
]);
required_unless – Required Except When
The inverse of required_if. A field is required unless another field has a specific value:
$request->validate([
'subscription' => 'required|in:free,basic,premium',
'card_token' => 'required_unless:subscription,free|string',
]);
card_token is required for basic and premium plans but not for free. The Rule::requiredUnless fluent form works the same way as Rule::requiredIf:
$request->validate([
'reason' => [
Rule::requiredUnless(fn () => $request->user()->is_admin),
'string',
'max:500',
],
]);
Required if Another Field Is Empty or Not Null
Two patterns that come up frequently:
Required if another field is empty – use required_without. When the field you depend on is absent or empty, yours becomes required:
$request->validate([
'username' => 'required_without:email|string|min:3',
'email' => 'required_without:username|email',
]);
If email is empty, username is required, and vice versa.
Required if not null – there is no built-in required_if_not_null rule, but required_with covers it. Since required_with triggers when the other field is present and not empty, it effectively means “required if the other field has a value”:
$request->validate([
'parent_id' => 'nullable|exists:categories,id',
'parent_order' => 'required_with:parent_id|integer|min:0',
]);
When parent_id has a value (not null, not absent), parent_order becomes required. When parent_id is null or missing, parent_order is optional.
For the inverse – “required when another field is null” – use Rule::requiredIf with a closure:
$request->validate([
'fallback_email' => [
Rule::requiredIf(fn () => null === $request->input('primary_email')),
'email',
],
]);
required_if vs. required_unless
Pick the one that makes the condition simpler to read. “Required if payment is credit card” is clearer than “required unless payment is bank or paypal.” When you have many exclusion values, required_if with a single inclusion value is shorter. When you have one exclusion, required_unless reads better.
required_with and required_without – Field Presence Checks
These rules check whether other fields are present and not empty in the request, regardless of what value they hold.
required_with – the field is required if any of the listed fields are present:
$request->validate([
'first_name' => 'required|string',
'last_name' => 'required|string',
'middle_name' => 'required_with:prefix,suffix|string|max:50',
]);
If the user provides a prefix or suffix, they must also provide middle_name. Useful for form sections that expand once the user starts filling optional parts.
required_without – the field is required if any of the listed fields are missing or empty:
$request->validate([
'email' => 'required_without:phone|email',
'phone' => 'required_without:email|string',
]);
This is the classic “provide at least one contact method” pattern. If email is empty or absent, phone becomes required, and vice versa. The validation ensures at least one of the two is present.
There are also required_with_all and required_without_all:
required_with_all – required only when every listed field is present:
$request->validate([
'latitude' => 'sometimes|numeric',
'longitude' => 'sometimes|numeric',
'radius' => 'required_with_all:latitude,longitude|numeric|min:1',
]);
A radius only makes sense when both coordinates are provided. If only latitude comes in without longitude, radius stays optional.
required_without_all – required when all listed fields are absent. This is the “at least one of” pattern for three or more fields:
$request->validate([
'email' => 'required_without_all:phone,fax|email',
'phone' => 'required_without_all:email,fax|string',
'fax' => 'required_without_all:email,phone|string',
]);
Each field is required only if both of the other two are missing. The user must provide at least one contact method, but any combination works.
required_if_accepted and required_if_declined
Two shortcuts for checkbox-driven conditional validation. required_if_accepted triggers when the other field is "yes", "on", 1, "1", true, or "true". required_if_declined triggers for "no", "off", 0, "0", false, or "false":
$request->validate([
'has_coupon' => 'sometimes|boolean',
'coupon_code' => 'required_if_accepted:has_coupon|string|max:20',
]);
Validating Checkboxes in Laravel
HTML checkboxes send their value only when checked. An unchecked checkbox sends nothing – not false, not 0, just absent from the request. This catches people off guard because required|boolean fails on an unchecked box (the field is missing, so required fails).
For a “terms and conditions” checkbox that must be checked:
$request->validate([
'terms' => 'accepted',
]);
The accepted rule passes for "yes", "on", 1, "1", true, and "true". It fails for everything else, including absent fields.
For an optional checkbox that can be either on or off:
$request->validate([
'newsletter' => 'sometimes|boolean',
]);
The sometimes rule skips validation when the field is absent, so an unchecked checkbox passes. When checked, the browser sends "on" by default, which the boolean rule rejects – its accepted values are true, false, 0, 1, '0', '1'. Normalize the value in prepareForValidation() only when the key was actually sent, so absent fields stay absent and sometimes still skips them:
protected function prepareForValidation(): void
{
if ($this->has('newsletter')) {
$this->merge([
'newsletter' => $this->boolean('newsletter'),
]);
}
}
$this->boolean('field') routes through filter_var(..., FILTER_VALIDATE_BOOLEAN) and turns "on", "yes", "true" into true, everything else into false. Without the has() guard, an unconditional merge() would inject the key with false even on requests where the checkbox was missing - silently defeating sometimes and forcing boolean to always run.
For conditional rules driven by a checkbox:
$request->validate([
'gift_wrap' => 'sometimes|boolean',
'gift_message' => 'required_if_accepted:gift_wrap|string|max:200',
]);
exclude_if and exclude_unless – Remove Fields from Validated Data
The exclude family goes further than required_if. These rules do not just skip validation – they remove the field from the array returned by $request->validated(). This prevents excluded data from leaking into your database when you pass validated data directly to Model::create().
$request->validate([
'has_appointment' => 'required|boolean',
'appointment_date' => 'exclude_if:has_appointment,false|required|date',
'doctor_name' => 'exclude_if:has_appointment,false|required|string',
]);
$validated = $request->validated();
// When has_appointment is false, $validated has no appointment_date or doctor_name keys
exclude_if – exclude when another field equals a value. exclude_unless – exclude unless another field equals a value. The logic mirrors required_if / required_unless:
$request->validate([
'account_type' => 'required|in:personal,business',
'company_name' => 'exclude_unless:account_type,business|required|string',
'vat_number' => 'exclude_unless:account_type,business|required|string',
]);
exclude if null
A common need: exclude a field when its companion is null. Use null as the value in the string syntax:
$request->validate([
'parent_id' => 'nullable|exists:categories,id',
'parent_slug' => 'exclude_if:parent_id,null|required|string',
]);
When parent_id is null, parent_slug disappears from the validated output entirely. Note: exclude_unless:parent_id,null does the inverse – it excludes parent_slug unless parent_id is null.
Rule::excludeIf for Complex Logic
Like Rule::requiredIf, the fluent form accepts a boolean or closure:
use Illuminate\Validation\Rule;
$request->validate([
'role_id' => [
Rule::excludeIf($request->user()->is_admin),
'exists:roles,id',
],
]);
Admin users do not submit a role – it is assigned automatically. The field is excluded from validated data so it cannot override the admin’s role through mass assignment.
There are also exclude_with and exclude_without variants that check for field presence rather than values, mirroring the required_with / required_without pair.
Why exclude Matters for Mass Assignment
Without exclude_if, conditional fields stay in the validated array even when they should not apply. Consider this:
// Without exclude_if
$validated = $request->validate([
'account_type' => 'required|in:personal,business',
'vat_number' => 'required_if:account_type,business|string',
]);
// $validated might contain ['account_type' => 'personal', 'vat_number' => 'XX123']
// The user sent a VAT number even though they selected "personal"
Company::create($validated); // VAT number saved for personal account
With exclude_unless, the field is stripped out when the condition does not match:
$validated = $request->validate([
'account_type' => 'required|in:personal,business',
'vat_number' => 'exclude_unless:account_type,business|required|string',
]);
// $validated is ['account_type' => 'personal'] – no vat_number key at all
Company::create($validated); // Clean – no stray data
This is defensive validation. Even if a user manipulates the form to send extra fields, exclude_* rules guarantee those fields never reach your model.
sometimes – Validate Only When Present
The sometimes rule tells Laravel to skip the entire rule chain for a field when it is not in the request data. Unlike nullable (which lets the value be null but still validates other rules), sometimes skips all validation when the key is missing:
$request->validate([
'name' => 'required|string',
'email' => 'sometimes|email|unique:users',
'bio' => 'sometimes|string|max:500',
]);
A PATCH request that only sends name passes – email and bio are not validated because they are absent. This is the standard pattern for partial updates.
sometimes vs. nullable. These two get confused regularly. nullable means “the field can be present with a null value and that is fine – skip remaining rules when null.” sometimes means “if the field is not in the request at all, skip everything.” They solve different problems and often appear together:
$request->validate([
'nickname' => 'sometimes|nullable|string|max:30',
]);
On a PATCH request where nickname is absent, sometimes skips validation entirely. When nickname is present but explicitly null (a JSON request with "nickname": null), nullable allows the null value through. When nickname is present with a string, string|max:30 validates it.
The sometimes() Method for Dynamic Rules
For conditional logic more complex than “validate when present,” use the sometimes() method on a Validator instance:
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Fluent;
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'games' => 'required|integer|min:0',
]);
$validator->sometimes('reason', 'required|max:500', function (Fluent $input) {
return $input->games >= 100;
});
The third argument is a closure that receives the input as a Fluent object. When it returns true, the rules are added. This works for multiple fields at once:
$validator->sometimes(
['reason', 'estimated_value'],
'required|string',
function (Fluent $input) {
return $input->games >= 100;
}
);
Conditional Validation in Nested Arrays
When you validate arrays and need the condition to reference a sibling field in the same array item, the closure receives a second argument – the current item:
$validator->sometimes(
'channels.*.address',
'email',
function (Fluent $input, Fluent $item) {
return 'email' === $item->type;
}
);
$validator->sometimes(
'channels.*.address',
'url',
function (Fluent $input, Fluent $item) {
return 'email' !== $item->type;
}
);
Each item in the channels array gets validated against a different rule depending on its type. The $item parameter is a Fluent instance when the attribute data is an array and a string otherwise. Without it, you would need a custom rule with DataAwareRule plus manual index tracking to achieve the same thing.
This pattern works well for dynamic forms where each row has a type selector that changes which fields are valid – notification preferences, polymorphic addresses, multi-channel contact forms.
Conditional Rules in Form Requests
All the string-based rules (required_if, exclude_if, etc.) work directly in a Form Request’s rules() method. For dynamic conditions, build them in the same method:
use Illuminate\Validation\Rule;
public function rules(): array
{
$isUpdate = 'PUT' === $this->method();
return [
'email' => [
$isUpdate ? 'sometimes' : 'required',
'email',
Rule::unique('users')->ignore($this->route('user')),
],
'password' => [
Rule::requiredIf(false === $isUpdate),
'string',
'min:8',
],
'role' => [
Rule::excludeUnless(fn () => $this->user()->is_admin),
'string',
Rule::in(['editor', 'viewer']),
],
];
}
This single Form Request handles both POST (create) and PUT (update). On create, email and password are required. On update, email is optional and password is not required. The role field is excluded from validated data unless the authenticated user is an admin – non-admins cannot escalate privileges even if they send the field.
For Form Requests that need the sometimes() method (the one with a closure), use the after() method to access the validator:
use Illuminate\Validation\Validator;
public function after(): array
{
return [
function (Validator $validator) {
$validator->sometimes('reason', 'required|string|max:500', function ($input) {
return 'reject' === $input->action;
});
},
];
}
The after() hook runs after the static rules from rules() are evaluated, giving you the validator instance for dynamic sometimes() calls.
Real-World Example: Multi-Step Checkout
A checkout form where shipping and billing rules depend on user choices. This combines most of the conditional techniques:
public function rules(): array
{
return [
// Step 1: account type
'account_type' => 'required|in:personal,business',
'company_name' => 'required_if:account_type,business|string|max:100',
'vat_number' => 'exclude_unless:account_type,business|required|string',
// Step 2: shipping
'shipping_method' => 'required|in:standard,express,pickup',
'address' => 'exclude_if:shipping_method,pickup|required|string',
'city' => 'exclude_if:shipping_method,pickup|required|string',
'postal_code' => 'exclude_if:shipping_method,pickup|required|string',
'pickup_location' => 'required_if:shipping_method,pickup|string',
// Step 3: payment
'payment_method' => 'required|in:card,invoice,free',
'card_token' => 'required_if:payment_method,card|string',
'invoice_email' => 'required_if:payment_method,invoice|email',
// Optional
'gift_wrap' => 'sometimes|boolean',
'gift_message' => 'required_if_accepted:gift_wrap|string|max:200',
'coupon' => 'sometimes|string|exists:coupons,code',
];
}
When shipping_method is pickup, the address fields are excluded from validated data entirely – they will not exist in the array passed to the order creation logic. The vat_number field only appears in validated data for business accounts. The gift_message is required only when the gift_wrap checkbox is checked.
Testing Conditional Validation
Conditional rules introduce branches that each need a test. Cover the “required” path and the “not required” path for each condition:
test('card token required for card payment', function () {
$this->postJson('/checkout', [
'payment_method' => 'card',
'card_token' => '',
])
->assertJsonValidationErrors(['card_token']);
});
test('card token not required for invoice payment', function () {
$this->postJson('/checkout', [
'payment_method' => 'invoice',
'invoice_email' => '[email protected]',
])
->assertJsonMissingValidationErrors(['card_token']);
});
For exclude_if, verify that excluded fields do not appear in the validated output. Test this at the Form Request level:
test('address excluded for pickup orders', function () {
$request = CheckoutRequest::create('/checkout', 'POST', [
'shipping_method' => 'pickup',
'pickup_location' => 'Store #5',
'address' => '123 Main St',
]);
$request->setContainer(app());
$validator = Validator::make($request->all(), (new CheckoutRequest)->rules());
$validated = $validator->validated();
expect($validated)->not->toHaveKey('address');
});
Common Mistakes
Mixing up required_if and exclude_if. required_if:status,active means “this field is required when status is active.” exclude_if:status,active means “drop this field from validated data when status is active.” They serve different purposes – one enforces presence, the other removes data. Use exclude_if when you want to prevent a field from reaching the database, not just when you want to make it optional.
Assuming required_if prevents other rules from running. When required_if:payment_type,cc is the first rule and payment_type is bank, the field is not required – but the remaining rules in the chain still apply if the field is present. If you validate tax_id with required_if:type,business|regex:/^[A-Z]{2}\d+$/, and the user sends a tax_id with an invalid format while type is personal, the regex rule still fires and returns an error. To skip all validation for that field, combine with exclude_if or sometimes.
required_without with two fields expecting “at least one.” required_without:email on phone and required_without:phone on email works for two fields. For three or more (“provide at least one of email, phone, or fax”), use required_without_all:email,phone on fax, and similarly on each field. Or write a custom rule that checks the group as a whole.
Conditional required with nullable. required_if:role,admin|nullable|string means: when role is admin the field must be present and non-empty; when role is anything else, the field is optional and may be null. The nullable only affects how the value is judged against type rules (here, string) when the condition is not met; it does not let null satisfy required_if. There is no rule combination that means “required but null is acceptable” - if the column needs to accept null in some cases, drop the required_* for those cases (use sometimes or split the form request) and enforce the constraint at the database layer.
Using exclude_if on the excluded field itself. If you put exclude_if:field,value where field references the same field being validated, the behavior is confusing. Always reference a different field as the condition.
Quick Reference
| Rule | Meaning |
|---|---|
required_if:field,value | Required when field equals value |
required_unless:field,value | Required unless field equals value |
required_with:field | Required when field is present and not empty |
required_with_all:a,b | Required when both a and b are present |
required_without:field | Required when field is absent or empty |
required_without_all:a,b | Required when both a and b are absent |
required_if_accepted:field | Required when field is truthy (checkbox “on”) |
required_if_declined:field | Required when field is falsy (checkbox “off”) |
exclude | Always excluded from validated data |
exclude_if:field,value | Excluded when field equals value |
exclude_unless:field,value | Excluded unless field equals value |
exclude_with:field | Excluded when field is present |
exclude_without:field | Excluded when field is absent |
sometimes | Skip all rules when field is absent from request |
The rules reference lists every built-in rule including the full required_* and exclude_* families. For checks that go beyond what string rules can express, build a custom rule. The validation basics guide covers controller validation, Form Requests, and Validator::make.