skip to content

In Laravel, where can you override a validation error message or the :attribute name, and in what order does the validator look for them?

level: middleimportance: must knowfreq 62%

answer

  1. inline beats the language file
  2. 'field.rule' before bare 'rule'
  3. validation.custom.field.rule
  4. size rules: per-type lines
  5. validation.attributes renames :attribute

basics

~20 s

Pass inline messages and attribute names (a form request's messages() and attributes(), or the extra validator arguments) or put them in lang/{locale}/validation.php under custom and attributes. Inline entries win, then custom, then the rule's default line.

solid answer

~40 s

For each failed rule the validator first checks the inline messages: the key `field.rule` (say `household_income.required`), then a bare `rule` key that applies to every field. Next it asks the translator for `validation.custom.{field}.{rule}` in the current locale's `validation.php`; for size rules such as `min` and `max` it then picks the per-type line (`validation.min.string`, `.numeric`, `.array`, `.file`), and otherwise the rule's default `validation.{rule}` line. The `:attribute` placeholder is resolved separately: inline `attributes()` first, then `validation.attributes`, and for a plain field the name snake-cased with underscores turned into spaces. Inline strings bypass the translator, so a bilingual form should keep its wording in `lang/{locale}/validation.php` or wrap `messages()` entries in `__()`.

code

php · 28 lines
php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class BenefitApplicationRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'household_income' => ['required', 'integer', 'min:0'],
            'national_id' => ['required', 'string', 'size:9'],
        ];
    }

    public function messages(): array
    {
        return [
            'household_income.required' => __('Tell us your total household income.'),
        ];
    }

    public function attributes(): array
    {
        return ['national_id' => __('national ID number')];
    }
}

go deeper

for a junior

Recall that messages() and attributes() on a form request, or the validator's extra arguments, change the wording, and that :attribute is the field's display name.

for a middle

Walk the lookup order: inline field.rule, inline rule, validation.custom, the size-rule type lines, then validation.{rule}; and the separate chain for :attribute.

for a senior

Explain why a translated form still shows English text (inline literals beat custom lines) and how to organise messages so every locale is covered without code changes.

for a principal

Decide where user-facing validation copy lives, code or language files, so content editors and translators can own it without touching form requests.

## Why a lookup chain exists Laravel ships a default English line for every built-in rule, such as `'required' => 'The :attribute field is required.'`. Those defaults live inside the framework (`Illuminate/Translation/lang/en/validation.php`), so validation produces readable messages even though the application skeleton has no `lang` directory. A real product, say a **government benefits application** offered in English and Spanish, needs friendlier wording: "Tell us your total household income" instead of "The household income field is required". Laravel lets you override messages at several levels and resolves them in a fixed order. ## The message lookup order For a failed rule on a field, `FormatsMessages::getMessage()` tries, in order: 1. **Inline, field and rule**: the key `household_income.required` in the messages array passed to the validator. With a form request that array is the return value of `messages()`; with a manual validator it is the third argument of `Validator::make()` or the second of `$request->validate()`. 2. **Inline, rule only**: a bare `required` key, which overrides that rule for every field. (A key equal to the field name holding an array of rule messages is also accepted.) 3. **Language file, custom**: `validation.custom.household_income.required` from `lang/{locale}/validation.php`. Wildcard field keys work here too. 4. **Size rules**: for `size`, `between`, `min`, `max`, `gt`, `lt`, `gte` and `lte`, the validator decides the value's type (numeric when a `numeric`, `integer` or `decimal` rule is present, array, file, otherwise string) and reads `validation.min.numeric`, `validation.min.string` and so on. 5. **Language file, default**: `validation.required`. 6. **Last resort**: the literal key, such as `validation.my_rule`, which is what you see when a custom rule name has no message anywhere. The rule name is snake-cased for lookup, so `requiredIf` becomes `required_if`. ## The chain in action Take `household_income` with the rules `required`, `integer` and `min:0`, and an applicant who types `-50`. The `required` and `integer` checks pass; `min` fails. No inline `household_income.min` or bare `min` entry exists, and the language file has no `custom.household_income.min`, so step 4 applies: the `integer` rule marks the value as numeric, the validator reads `validation.min.numeric` ("The :attribute field must be at least :min."), and the placeholders turn it into "The household income field must be at least 0." Adding one inline `household_income.min` entry would short-circuit the whole chain at step 1. ## Naming the field: the `:attribute` placeholder The field's display name is resolved independently of the message text: | Source | Example entry | Result in the default line | |---|---|---| | Inline `attributes()` / 4th validator argument | `'national_id' => 'national ID number'` | The national ID number field is required. | | `validation.attributes` in the language file | `'national_id' => 'número de identificación'` | El campo número de identificación es obligatorio. | | Neither (plain field) | none | The national id field is required. | For a plain field the fallback is `Str::snake()` followed by replacing underscores with spaces, so `householdIncome` also becomes "household income". The capitalised variants `:Attribute` and `:ATTRIBUTE` exist for messages that start with the field name. Attribute names change **only the text**: the errors stay keyed by the real field name. ## Other placeholders worth knowing - `:input` inserts the submitted value when it is scalar, useful for "`:input` is not a valid postcode". - `:other` and `:value` appear in rules such as `required_if`; the language file's `values` array can map a raw value (`cc`) to a label (`credit card`). - Rule parameters such as `:min`, `:max` and `:date` are filled in by per-rule replacers. ## Translating a benefits form The translator reads `validation.php` for the **current locale**, and when a key is missing there it falls back to `fallback_locale` (the skeleton sets `APP_FALLBACK_LOCALE` to `en`). Setting the locale and publishing the language files are localization topics; what matters here is where the strings sit: - **Language-file strategy**: keep wording in `lang/en/validation.php` and `lang/es/validation.php` under `custom` and `attributes`. Every locale gets its own text with no code change. - **Inline strategy**: `messages()` and `attributes()` are plain PHP arrays. A literal English string there is **not translated**; wrap it in `__()` so it is looked up in the locale's JSON translations. - **Mixing both**: inline entries win. An English string left in `messages()` silently beats the Spanish `custom` entry, the classic reason a translated form shows one stubborn English message. ## Pitfalls - A bare `required` key in `messages()` rewrites that rule for **all** fields, sometimes by accident. - Custom rule names without any message surface as `validation.rule_name` to users. - `:input` echoes user data; print messages with `{{ }}` so it stays escaped. - `attributes()` does not rename the key used by `@error` or in a JSON body.

  • If `messages()` contains a bare `'required' => 'This is needed.'` entry, which failures does it affect?
    Every `required` failure on every field, except where a more specific inline `field.required` key exists, because the validator tries `{field}.{rule}` before `{rule}`. It also beats any `validation.custom` entry in the language file, since inline messages are consulted before the translator.
  • Why does the `min` rule have separate messages for strings, numbers, arrays and files?
    `min` is a size rule, and size means characters, value, item count or kilobytes depending on the data. The validator infers the type (numeric if a `numeric`, `integer` or `decimal` rule is present, then array, file, otherwise string) and reads `validation.min.{type}`. An inline override for a size rule may be an array keyed by those types.
  • What does the `:input` placeholder add, and what must you watch for?
    It substitutes the submitted value when it is scalar, so a message can say "12345 is not a valid postcode". Because that is raw user input, the message must be echoed escaped with `{{ }}`; printing it raw with `{!! !!}` would turn the error message into a reflected XSS vector.

saying these in an interview costs you the question

  • Language-file custom messages override the ones returned by a form request's messages().
  • Strings returned from messages() are translated to the current locale automatically.
  • Without publishing the lang directory Laravel shows no validation messages at all.
  • attributes() renames the key under which the field's errors are stored.
  • The :attribute placeholder always prints the raw input name, underscores included.