Skip to main content

Conditional Validation Detailed Guide

Laravel Spectrum automatically detects complex conditional validation rules and generates appropriate OpenAPI 3.0 schemas. This guide explains in detail the supported patterns and generated documentation.

🎯 Overview​

Conditional validation refers to applying different validation rules based on specific conditions. Laravel Spectrum supports:

  • HTTP method-based conditions
  • Field value-based conditions
  • Authentication state-based conditions
  • Custom logic-based conditions

📋 Basic Patterns​

HTTP Method Based Conditions​

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ProductRequest extends FormRequest
{
public function rules()
{
$rules = [
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
];

switch ($this->method()) {
case 'POST':
// All fields required on creation
$rules['sku'] = 'required|string|unique:products,sku';
$rules['price'] = 'required|numeric|min:0';
$rules['stock'] = 'required|integer|min:0';
$rules['category_id'] = 'required|exists:categories,id';
break;

case 'PUT':
// Full update
$rules['sku'] = 'required|string|unique:products,sku,' . $this->route('product');
$rules['price'] = 'required|numeric|min:0';
$rules['stock'] = 'required|integer|min:0';
$rules['category_id'] = 'required|exists:categories,id';
break;

case 'PATCH':
// Partial update - all optional
$rules['sku'] = 'sometimes|string|unique:products,sku,' . $this->route('product');
$rules['price'] = 'sometimes|numeric|min:0';
$rules['stock'] = 'sometimes|integer|min:0';
$rules['category_id'] = 'sometimes|exists:categories,id';
break;
}

return $rules;
}
}

Generated OpenAPI Schema:

requestBody:
content:
application/json:
schema:
oneOf:
- title: "Create Product (POST)"
type: object
required: ["name", "sku", "price", "stock", "category_id"]
properties:
name:
type: string
maxLength: 255
description:
type: string
maxLength: 1000
nullable: true
sku:
type: string
price:
type: number
minimum: 0
stock:
type: integer
minimum: 0
category_id:
type: integer

- title: "Update Product (PUT)"
type: object
required: ["name", "sku", "price", "stock", "category_id"]
properties:
# Same structure

- title: "Partial Update (PATCH)"
type: object
required: ["name"]
properties:
# All fields optional (except name)

Field Value Based Conditions​

public function rules()
{
return [
'user_type' => 'required|in:individual,company,government',

// For individual users
'first_name' => 'required_if:user_type,individual|string|max:100',
'last_name' => 'required_if:user_type,individual|string|max:100',
'date_of_birth' => 'required_if:user_type,individual|date|before:-18 years',
'ssn' => 'required_if:user_type,individual|regex:/^\d{3}-\d{2}-\d{4}$/',

// For companies
'company_name' => 'required_if:user_type,company,government|string|max:255',
'registration_number' => 'required_if:user_type,company|string',
'tax_id' => 'required_if:user_type,company|string',
'incorporation_date' => 'required_if:user_type,company|date|before:today',

// For government agencies
'agency_name' => 'required_if:user_type,government|string',
'department' => 'required_if:user_type,government|string',
'government_id' => 'required_if:user_type,government|string',

// Common optional fields
'phone' => 'nullable|string|regex:/^\+?[1-9]\d{1,14}$/',
'website' => 'nullable|url',
'address' => 'required|array',
'address.street' => 'required|string',
'address.city' => 'required|string',
'address.postal_code' => 'required|string',
'address.country' => 'required|string|size:2',
];
}

🔄 Complex Conditional Patterns​

Using Rule::when​

use Illuminate\Validation\Rule;

public function rules()
{
return [
'subscription_type' => 'required|in:free,basic,premium,enterprise',

'payment_method' => Rule::when(
$this->subscription_type !== 'free',
['required', 'in:credit_card,paypal,bank_transfer,invoice'],
'nullable'
),

'billing_cycle' => Rule::when(
in_array($this->subscription_type, ['basic', 'premium']),
['required', 'in:monthly,yearly'],
Rule::when(
$this->subscription_type === 'enterprise',
['required', 'in:monthly,quarterly,yearly,custom'],
'nullable'
)
),

'invoice_details' => Rule::when(
$this->payment_method === 'invoice' && $this->subscription_type === 'enterprise',
['required', 'array'],
'nullable'
),

'invoice_details.company_name' => Rule::when(
$this->payment_method === 'invoice',
'required|string',
'nullable'
),

'invoice_details.tax_id' => Rule::when(
$this->payment_method === 'invoice',
'required|string',
'nullable'
),
];
}

Multiple Condition Combinations​

public function rules()
{
$rules = [
'event_type' => 'required|in:online,offline,hybrid',
'start_date' => 'required|date|after:today',
'end_date' => 'required|date|after:start_date',
'max_attendees' => 'required|integer|min:1',
];

// For offline or hybrid events
if (in_array($this->event_type, ['offline', 'hybrid'])) {
$rules['venue'] = 'required|array';
$rules['venue.name'] = 'required|string';
$rules['venue.address'] = 'required|string';
$rules['venue.capacity'] = 'required|integer|gte:max_attendees';
$rules['venue.facilities'] = 'array';
$rules['venue.facilities.*'] = 'in:parking,wifi,catering,av_equipment';

// COVID-19 measures (conditional)
if ($this->requires_safety_measures) {
$rules['safety_measures'] = 'required|array';
$rules['safety_measures.mask_required'] = 'required|boolean';
$rules['safety_measures.vaccination_required'] = 'required|boolean';
$rules['safety_measures.capacity_limit'] = 'required|integer|lte:venue.capacity';
}
}

// For online or hybrid events
if (in_array($this->event_type, ['online', 'hybrid'])) {
$rules['streaming'] = 'required|array';
$rules['streaming.platform'] = 'required|in:zoom,teams,youtube,custom';
$rules['streaming.url'] = 'required_if:streaming.platform,custom|url';
$rules['streaming.password'] = 'nullable|string';
$rules['streaming.recording_available'] = 'boolean';

// Platform-specific settings
if ($this->input('streaming.platform') === 'zoom') {
$rules['streaming.meeting_id'] = 'required|string';
$rules['streaming.passcode'] = 'nullable|string|size:6';
}
}

return $rules;
}

🎨 Authentication-Based Conditions​

public function rules()
{
$user = $this->user();
$rules = [
'title' => 'required|string|max:255',
'content' => 'required|string',
'status' => 'required|in:draft,pending,published',
];

// Additional fields for administrators
if ($user && $user->hasRole('admin')) {
$rules['featured'] = 'boolean';
$rules['priority'] = 'integer|between:1,10';
$rules['published_at'] = 'nullable|date';
$rules['author_id'] = 'nullable|exists:users,id';
$rules['internal_notes'] = 'nullable|string';
}

// For editors
if ($user && $user->hasRole('editor')) {
$rules['tags'] = 'array|max:10';
$rules['tags.*'] = 'string|exists:tags,name';
$rules['category_id'] = 'required|exists:categories,id';
}

// Restrictions for regular users
if ($user && $user->hasRole('user')) {
$rules['status'] = 'required|in:draft,pending'; // Cannot select published
$rules['visibility'] = 'required|in:private,friends'; // Cannot select public
}

return $rules;
}

🔧 Dynamic Validation​

Database State Based Conditions​

public function rules()
{
$product = Product::find($this->route('product'));

$rules = [
'quantity' => [
'required',
'integer',
'min:1',
// Less than or equal to stock
'max:' . ($product ? $product->stock : 0),
],
];

// For pre-order products
if ($product && $product->is_preorder) {
$rules['delivery_date'] = 'required|date|after:' . $product->release_date;
$rules['deposit_amount'] = 'required|numeric|min:' . ($product->price * 0.2);
}

// For limited products
if ($product && $product->is_limited) {
$existingOrders = $this->user()
->orders()
->whereHas('items', function ($q) use ($product) {
$q->where('product_id', $product->id);
})
->sum('quantity');

$rules['quantity'] .= '|max:' . ($product->limit_per_customer - $existingOrders);
}

return $rules;
}

External API or Service Based Conditions​

public function rules()
{
$rules = [
'shipping_address' => 'required|array',
'shipping_address.country' => 'required|string|size:2',
'shipping_address.postal_code' => 'required|string',
];

// Check available shipping regions
$shippingService = app(ShippingService::class);
$availableServices = $shippingService->getAvailableServices(
$this->input('shipping_address.country'),
$this->input('shipping_address.postal_code')
);

if (!empty($availableServices)) {
$rules['shipping_method'] = [
'required',
Rule::in(array_keys($availableServices))
];

// Specific shipping methods require additional information
if ($this->shipping_method === 'express') {
$rules['delivery_instructions'] = 'required|string|max:500';
$rules['contact_phone'] = 'required|string';
}
}

return $rules;
}

📊 OpenAPI Generation Details​

oneOf Schema Generation​

Laravel Spectrum generates OpenAPI 3.0 oneOf schemas when detecting conditional validation:

components:
schemas:
UserRequest:
oneOf:
- $ref: '#/components/schemas/IndividualUserRequest'
- $ref: '#/components/schemas/CompanyUserRequest'
- $ref: '#/components/schemas/GovernmentUserRequest'
discriminator:
propertyName: user_type
mapping:
individual: '#/components/schemas/IndividualUserRequest'
company: '#/components/schemas/CompanyUserRequest'
government: '#/components/schemas/GovernmentUserRequest'

IndividualUserRequest:
type: object
required: [user_type, first_name, last_name, date_of_birth, ssn]
properties:
user_type:
type: string
enum: [individual]
first_name:
type: string
maxLength: 100
last_name:
type: string
maxLength: 100
date_of_birth:
type: string
format: date
ssn:
type: string
pattern: '^\d{3}-\d{2}-\d{4}$'

Condition Description Generation​

properties:
payment_method:
type: string
enum: [credit_card, paypal, bank_transfer, invoice]
description: "Required when subscription_type is not 'free'"
x-condition: "subscription_type !== 'free'"

billing_cycle:
type: string
enum: [monthly, quarterly, yearly, custom]
description: "Required for paid subscriptions. Enterprise allows custom billing."
x-conditions:
- when: "subscription_type in ['basic', 'premium']"
enum: [monthly, yearly]
- when: "subscription_type === 'enterprise'"
enum: [monthly, quarterly, yearly, custom]

💡 Best Practices​

1. Clear Condition Descriptions​

// ✅ Good example: Clear conditions
public function rules()
{
$isPaidPlan = in_array($this->plan, ['basic', 'pro', 'enterprise']);

return [
'plan' => 'required|in:free,basic,pro,enterprise',
'payment_method' => $isPaidPlan ? 'required|string' : 'nullable',
'billing_address' => $isPaidPlan ? 'required|array' : 'nullable',
];
}

// ❌ Bad example: Complex and hard to understand conditions
public function rules()
{
return [
'payment_method' => ($this->plan !== 'free' && !$this->is_trial && $this->user()->subscription_expired) ? 'required' : 'nullable',
];
}

2. Custom Validation Messages​

public function messages()
{
return [
'company_name.required_if' => 'Company name is required for business accounts.',
'ssn.required_if' => 'SSN is required for individual accounts.',
'payment_method.required' => 'Payment method is required for paid subscriptions.',
];
}

3. Grouping Conditions​

protected function personalInfoRules(): array
{
return [
'first_name' => 'required|string',
'last_name' => 'required|string',
'date_of_birth' => 'required|date',
];
}

protected function businessInfoRules(): array
{
return [
'company_name' => 'required|string',
'tax_id' => 'required|string',
];
}

public function rules()
{
$rules = ['user_type' => 'required|in:personal,business'];

if ($this->user_type === 'personal') {
$rules = array_merge($rules, $this->personalInfoRules());
} else {
$rules = array_merge($rules, $this->businessInfoRules());
}

return $rules;
}

🔍 Troubleshooting​

When Conditions Are Not Detected​

  1. Check that methods are public

    public function rules() // Must be public
  2. Check that conditions are statically analyzable

    // ✅ Detectable
    if ($this->isMethod('POST')) { }

    // ❌ Difficult to detect
    if ($this->someComplexMethod()) { }
  3. Clear cache

    php artisan spectrum:cache clear