How to Create Laravel Custom Validation Rules with Field-Specific Error Messages
Laravel's validation engine automatically maps every error to its specific attribute through the addFailure() method in Validator.php, enabling field-specific messages via custom rule classes, form request configurations, or attribute aliases.
When building robust applications with the laravel/framework repository, implementing Laravel custom validation rules requires careful attention to error message specificity. The framework's validation architecture ensures that every failure is tied to its exact field, even in complex scenarios involving nested arrays or wildcard patterns.
Understanding Laravel's Validation Error Architecture
How Errors Are Mapped to Attributes
In src/Illuminate/Validation/Validator.php, the addFailure() method (lines 998-1019) serves as the central mechanism for attaching errors to specific fields. When any validation rule fails, the validator calls this method with the raw attribute name (e.g., users.0.email), resolves any placeholder hashes, and adds the message to the internal MessageBag under that exact key:
$this->messages->add($attribute, $this->makeReplacements(
$this->getMessage($attributeWithPlaceholders, $rule), $attribute, $rule, $parameters
));
Because the key is the fully-qualified attribute, Laravel can later retrieve a field-specific message when you call $validator->errors() or $request->validated().
The Role of Custom Rule Objects
For Laravel custom validation rules implementing \Illuminate\Contracts\Validation\Rule, the validateUsingCustomRule() method (lines 1515-1550 in Validator.php) handles message resolution. After the rule's passes() method returns false, the validator builds the message through a specific hierarchy:
- It first searches for a message defined for the attribute-rule pair in the validator's local array (
$this->customMessages). - If none exists, it falls back to the rule's own
message()method.
$messages = $this->getFromLocalArray($originalAttribute, $ruleClass) ?? $rule->message();
Thus, the source of specificity is the attribute key that travels through the validator, ensuring that even custom rules produce field-specific errors.
Implementing Field-Specific Messages in Laravel Custom Validation Rules
Method 1: Custom Rule Classes with Dynamic Messages
When creating a custom rule class, the message() method receives the attribute name, enabling you to compose dynamic, field-specific text:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class Uppercase implements Rule
{
public function passes($attribute, $value)
{
return strtoupper($value) === $value;
}
// The $attribute argument is the exact field that failed.
public function message()
{
return "The :attribute must be written in uppercase.";
}
}
Usage in a Form Request:
public function rules()
{
return [
'name' => ['required', new Uppercase],
'email' => ['required', 'email'],
];
}
If name fails, the validator adds an entry under the key name, so $request->errors()->first('name') returns the custom message.
Method 2: Per-Field Messages in Form Requests
Override the messages() method in your form request to define specific error text for individual fields:
public function messages()
{
return [
'email.required' => 'We need to know your email address.',
'email.email' => 'The email format looks odd – double‑check it.',
'users.*.age.min'=> 'Each user must be at least :min years old.',
];
}
The users.*.age.min entry automatically matches any expanded key such as users.0.age or users.1.age.
Method 3: Wildcard Validation for Nested Arrays
When validating nested array structures, use wildcard notation in both your rules and messages to maintain field specificity:
public function rules()
{
return [
'users.*.email' => 'required|email',
'users.*.age' => 'required|integer|min:18',
];
}
public function messages()
{
return [
'users.*.email.required' => 'User email is mandatory.',
'users.*.age.min' => 'Users must be adults (18+).',
];
}
Method 4: Friendly Attribute Names
Change the display name of fields using the attributes() method to improve message clarity without modifying the message strings:
public function attributes()
{
return [
'email' => 'email address',
'users.*.age' => 'age of each user',
];
}
Now the :attribute placeholder inside any message (including those from custom rules) will be replaced with the friendly string.
Complex Validation Scenarios
Handling Nested Array Validation
For deeply nested structures, Laravel's validator expands wildcard patterns before calling addFailure(). This means you can define messages for users.*.profile.bio.required and Laravel will correctly map failures at users.0.profile.bio to your custom text.
Dynamic Rule Parameters
Custom rule objects receive the attribute name in the passes() method, enabling conditional logic based on the field being validated:
public function passes($attribute, $value)
{
// You can access the specific field name here
if ($attribute === 'admin_code') {
return $this->validateAdminFormat($value);
}
return $this->validateStandardFormat($value);
}
Key Source Files in laravel/framework
| File | Purpose | Location |
|---|---|---|
src/Illuminate/Validation/Validator.php – addFailure() |
Adds error messages to the MessageBag under the exact attribute key. |
View source |
src/Illuminate/Validation/Validator.php – validateUsingCustomRule() |
Retrieves custom rule messages while preserving the original attribute name. | View source |
src/Illuminate/Validation/Concerns/FormatsMessages.php |
Handles placeholder replacement (:attribute, :value, etc.) in error messages. |
View source |
src/Illuminate/Validation/Concerns/ValidatesAttributes.php |
Provides the validate* methods that trigger addFailure when checks fail. |
View source |
Summary
- Laravel stores every validation error under its specific attribute key via
addFailure()inValidator.php, ensuring field-specific messaging. - Custom rule objects automatically receive attribute context through
validateUsingCustomRule(), allowing dynamic message generation. - Per-field messages can be defined in form requests using
field.rulenotation or wildcard patterns likeusers.*.email.required. - Friendly attribute names set via
attributes()improve message clarity by replacing the:attributeplaceholder. - Nested array validation maintains specificity through wildcard expansion before error storage.
Frequently Asked Questions
How does Laravel associate validation errors with specific fields?
Laravel's Validator class uses the addFailure() method (lines 998-1019 in src/Illuminate/Validation/Validator.php) to store each error message under the exact attribute key that failed validation. Whether validating simple fields like email or nested paths like users.0.profile.name, the validator resolves the full attribute path before adding the message to the internal MessageBag.
Can I display different error messages for the same rule on different fields?
Yes. In a form request, override the messages() method and use dot notation to target specific field-rule combinations. For example, define 'email.required' and 'name.required' with different strings. Alternatively, implement the message() method in your custom rule class to return dynamic text based on the $attribute parameter passed to the passes() method.
How do I handle validation errors for nested array fields?
Use wildcard notation in both your validation rules and custom messages. Define rules like users.*.email and corresponding messages like users.*.email.required. Laravel expands these wildcards to concrete indices (e.g., users.0.email) before calling addFailure(), ensuring that errors appear under the specific nested key. You can also use the attributes() method to map users.*.email to a friendly name like "user email address."
What is the difference between the messages() and attributes() methods in form requests?
The messages() method allows you to define the actual error text for specific field-rule combinations, such as returning ['email.required' => 'We need your email']. The attributes() method changes the display name of the field itself by modifying the :attribute placeholder replacement, such as mapping email to "email address" so that standard messages read "The email address field is required" instead of "The email field is required."
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →