Backend Development

Laravel Validation: Custom Rules and Messages

Mayur Dabhi
Mayur Dabhi
April 6, 2026
13 min read

Validation is the first line of defence for any web application. Bad data leads to broken logic, security vulnerabilities, and corrupted databases. Laravel's validation system is one of the most elegant in any framework — but most developers only scratch the surface of what it can do. In this guide we'll go from the basics of inline validation all the way to custom Rule objects, Form Requests, and conditional logic that handles real-world complexity.

Why Laravel Validation?

Laravel provides 90+ built-in validation rules out of the box, automatic redirect-with-errors on failure, seamless integration with Blade templates, and a clean API for adding your own domain-specific rules. You rarely need to reach for a third-party library.

Built-in Validation Rules

The simplest way to validate incoming data is to call $request->validate() inside a controller method. If validation fails Laravel automatically redirects the user back with the errors flashed to the session — no extra code needed.

PHP — Controller inline validation
public function store(Request $request)
{
    $validated = $request->validate([
        'name'     => 'required|string|max:100',
        'email'    => 'required|email|unique:users,email',
        'age'      => 'required|integer|min:18|max:120',
        'website'  => 'nullable|url',
        'role'     => 'required|in:admin,editor,viewer',
        'password' => 'required|string|min:8|confirmed',
        'avatar'   => 'nullable|image|mimes:jpg,png,webp|max:2048',
        'tags'     => 'nullable|array|max:5',
        'tags.*'   => 'string|max:30',
    ]);

    User::create($validated);
    return redirect()->route('users.index')->with('success', 'User created.');
}

Here is a quick reference of the most commonly used built-in rules:

RulePurposeExample
requiredField must be present and non-empty'name' => 'required'
stringMust be a string'bio' => 'string'
integerMust be an integer'age' => 'integer'
numericMust be numeric (int or float)'price' => 'numeric'
emailMust be a valid email address'email' => 'email'
urlMust be a valid URL'website' => 'url'
min:n / max:nLength or value bounds'password' => 'min:8'
unique:table,colMust not exist in database'email' => 'unique:users'
exists:table,colMust exist in database'category_id' => 'exists:categories,id'
in:a,b,cMust be one of the listed values'status' => 'in:active,inactive'
confirmedMust have matching _confirmation field'password' => 'confirmed'
imageMust be an image file'avatar' => 'image'
arrayMust be an array'tags' => 'array'
dateMust be a valid date'dob' => 'date'
regex:patternMust match a regex'phone' => 'regex:/^[0-9]{10}$/'

Custom Error Messages

Default Laravel error messages are sensible but generic. For a polished user experience you'll want to override them with messages that are specific to your domain.

Inline Message Overrides

Pass a second array to validate() with keys in the format field.rule:

PHP — Custom messages in controller
$request->validate(
    [
        'email'    => 'required|email|unique:users',
        'password' => 'required|min:8|confirmed',
        'age'      => 'required|integer|min:18',
    ],
    [
        'email.required'    => 'We need your email address to create your account.',
        'email.unique'      => 'That email is already registered. Try logging in instead.',
        'password.min'      => 'Your password must be at least 8 characters long.',
        'password.confirmed'=> 'The passwords you entered do not match.',
        'age.min'           => 'You must be at least 18 years old to register.',
    ]
);

Custom Attribute Names

By default Laravel uses the field name in error messages. Pass a third array to rename attributes to something human-friendly:

PHP — Custom attribute names
$request->validate(
    ['dob' => 'required|date|before:today'],
    ['dob.required' => 'Please enter your date of birth.'],
    ['dob' => 'date of birth']  // Used in default messages: "The date of birth field is required."
);

Global Translations

For app-wide overrides, edit lang/en/validation.php. The attributes key at the bottom lets you map field names globally without repeating yourself across controllers:

lang/en/validation.php
// At the bottom of the file:
'attributes' => [
    'email'       => 'email address',
    'dob'         => 'date of birth',
    'phone_number'=> 'phone number',
    'zip_code'    => 'ZIP code',
],

Form Request Classes

For any non-trivial form, inline validation in the controller quickly gets messy. Form Requests are dedicated classes that encapsulate your validation logic, keeping controllers slim and making rules reusable and testable.

1

Generate the Form Request

php artisan make:request StoreUserRequest — creates app/Http/Requests/StoreUserRequest.php.

2

Define the authorize() method

Return true to allow all users, or add policy/role checks here. Returning false gives a 403.

3

Define the rules() method

Return your validation rules array — the same format as inline validation.

4

Type-hint it in the controller

Replace Request $request with StoreUserRequest $request. Validation runs automatically before your controller method is called.

app/Http/Requests/StoreUserRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;

class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        // Only authenticated admins can create users
        return $this->user()?->isAdmin() ?? false;
    }

    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:100'],
            'email'    => ['required', 'email', 'unique:users,email'],
            'password' => ['required', 'confirmed', Password::min(8)
                            ->mixedCase()
                            ->numbers()
                            ->symbols()],
            'role'     => ['required', 'in:admin,editor,viewer'],
            'avatar'   => ['nullable', 'image', 'max:2048'],
        ];
    }

    public function messages(): array
    {
        return [
            'email.unique'   => 'That email address is already taken.',
            'password.confirmed' => 'Passwords do not match.',
        ];
    }

    public function attributes(): array
    {
        return [
            'email' => 'email address',
        ];
    }
}
app/Http/Controllers/UserController.php
use App\Http\Requests\StoreUserRequest;

public function store(StoreUserRequest $request)
{
    // $request->validated() returns only the validated fields
    $user = User::create($request->validated());
    return redirect()->route('users.show', $user)->with('success', 'User created!');
}

Custom Validation Rules (Rule Objects)

When none of the 90+ built-in rules fit your requirement, create a Rule object. This is ideal for rules you'll reuse across multiple forms — like validating a phone number format, checking a VAT number, or enforcing a business-specific password policy.

Terminal
php artisan make:rule StrongPassword
app/Rules/StrongPassword.php
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class StrongPassword implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strlen($value) < 8) {
            $fail('The :attribute must be at least 8 characters.');
            return;
        }

        if (!preg_match('/[A-Z]/', $value)) {
            $fail('The :attribute must contain at least one uppercase letter.');
            return;
        }

        if (!preg_match('/[0-9]/', $value)) {
            $fail('The :attribute must contain at least one number.');
            return;
        }

        if (!preg_match('/[\W_]/', $value)) {
            $fail('The :attribute must contain at least one special character.');
        }
    }
}

Use it just like any built-in rule by instantiating it in your rules array:

PHP — Using a custom Rule object
use App\Rules\StrongPassword;

$request->validate([
    'password' => ['required', 'confirmed', new StrongPassword()],
]);
Rule objects vs closures

Use a Rule class when you need the rule in multiple places. Use a closure (shown next) for one-off validation that lives in a single controller or form request.

Closure-Based Custom Rules

For a quick one-off rule that doesn't warrant its own class, pass a closure directly into the rules array:

PHP — Closure validation rule
use Illuminate\Validation\Validator;

$request->validate([
    'username' => [
        'required',
        'string',
        'max:30',
        function (string $attribute, mixed $value, Closure $fail) {
            if (str_contains(strtolower($value), 'admin')) {
                $fail("The {$attribute} cannot contain the word 'admin'.");
            }

            if (!preg_match('/^[a-z0-9_]+$/', $value)) {
                $fail("The {$attribute} may only contain lowercase letters, numbers, and underscores.");
            }
        },
    ],
]);

Conditional Validation

Real-world forms have fields that are only required depending on other fields. Laravel handles this elegantly with required_if, required_with, and the sometimes() method on the Validator facade.

PHP — Conditional rules
$request->validate([
    'payment_method'  => 'required|in:card,bank_transfer,crypto',

    // Only required when paying by card
    'card_number'     => 'required_if:payment_method,card|digits:16',
    'card_expiry'     => 'required_if:payment_method,card|date_format:m/y',
    'card_cvv'        => 'required_if:payment_method,card|digits:3',

    // Required when either field is present
    'bank_name'       => 'required_with:bank_account|string|max:100',
    'bank_account'    => 'required_with:bank_name|string|max:30',

    // Required unless another field has a specific value
    'shipping_address'=> 'required_unless:delivery_type,digital',
]);

For complex conditional logic use Validator::make() with sometimes():

PHP — Validator::sometimes()
use Illuminate\Support\Facades\Validator;

$validator = Validator::make($request->all(), [
    'country' => 'required|string|size:2',
    'zip'     => 'required|string',
]);

// Add extra rule only for US addresses
$validator->sometimes('zip', 'digits:5', function ($input) {
    return $input->country === 'US';
});

// Add extra rule only for UK addresses
$validator->sometimes('zip', 'regex:/^[A-Z]{1,2}[0-9R][0-9A-Z]? [0-9][ABD-HJLNP-UW-Z]{2}$/i', function ($input) {
    return $input->country === 'GB';
});

$validator->validate();
HTTP Request POST /users Form Request authorize() + rules() Redirect + Errors 422 / back() FAIL PASS Controller $request->validated() Response 200 OK / redirect

Laravel validation flow: request → Form Request (authorize + rules) → controller → response

Advanced Techniques

Stop on First Failure

By default Laravel runs all rules and returns all errors at once. Use stopOnFirstFailure() on a Validator instance to halt after the first failing rule:

PHP — stopOnFirstFailure
$validator = Validator::make($request->all(), $rules);
$validator->stopOnFirstFailure()->validate();

Array Validation

Validating each element of a submitted array uses the * wildcard:

PHP — Array field validation
$request->validate([
    'items'           => 'required|array|min:1|max:20',
    'items.*.product_id' => 'required|integer|exists:products,id',
    'items.*.quantity'   => 'required|integer|min:1|max:100',
    'items.*.note'       => 'nullable|string|max:200',
]);

After Validation Hooks

Add custom logic that runs after all rules pass using after(). Useful for cross-field checks that span multiple tables:

PHP — after() hook
$validator = Validator::make($request->all(), $rules);

$validator->after(function ($validator) use ($request) {
    $start = Carbon::parse($request->start_date);
    $end   = Carbon::parse($request->end_date);

    if ($end->lessThanOrEqualTo($start)) {
        $validator->errors()->add('end_date', 'End date must be after start date.');
    }
});

$validator->validate();

Key Takeaways

  • Use $request->validate() for simple forms in controllers
  • Move validation to Form Request classes as soon as rules get complex
  • Create Rule objects for reusable domain-specific validation
  • Use closures for quick one-off rules inside a single form
  • Always use $request->validated() (not all()) to safely mass-assign data
  • Leverage required_if, required_with, and sometimes() for conditional logic
  • Use after() hooks for cross-field business rules that span multiple values
Laravel Validation PHP Forms Backend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.