In Laravel, validating `dependents.*.birth_date` produces "The dependents.1.birth_date field is required." How do you make that message readable for users?
answer
- wildcard fields show their raw path
- attributes() accepts wildcard keys
- :position counts from 1
- :index counts from 0
- :ordinal-position needs intl
basics
~10 sArray fields print their raw dot path as :attribute unless named. Map 'dependents..birth_date' in attributes() or validation.attributes, or write a wildcard message such as 'dependents..birth_date.required' using :position to say which row failed.
solid answer
~40 sThe validator expands `dependents.*.birth_date` into concrete keys such as `dependents.1.birth_date`, and when no custom name exists it prints that raw path as `:attribute`; the underscores-to-spaces fallback applies only to plain fields. Custom names and messages accept wildcard keys, so `attributes()` can return `['dependents.*.birth_date' => 'date of birth']`, but then every row reads the same. To say which row failed, write a wildcard message with a position placeholder: `:index` is zero-based, `:position` starts at 1, `:ordinal-position` gives "2nd" but needs the `intl` extension, and `:second-position` addresses a deeper wildcard. The errors stay keyed by dot paths, so Blade must ask for `dependents.1.birth_date`, not the HTML name `dependents[1][birth_date]`.
code
php · 29 lines<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class HouseholdRequest extends FormRequest
{
public function rules(): array
{
return [
'dependents' => ['array'],
'dependents.*.name' => ['required', 'string'],
'dependents.*.birth_date' => ['required', 'date', 'before:today'],
];
}
public function messages(): array
{
return [
'dependents.*.birth_date.required' => 'Enter a date of birth for dependent :position.',
];
}
public function attributes(): array
{
return ['dependents.*.name' => 'dependent name'];
}
}go deeper
Recall that array inputs report errors under dot paths such as dependents.1.birth_date, and that @error needs that exact key.
Explain wildcard keys in messages() and attributes() and the :index versus :position placeholders.
Anticipate the environment and translation traps: the intl dependency of :ordinal-position, inline strings overriding translated wildcard messages, and client code mapping dot paths back to inputs.
Decide whether repeating-row forms should report per-row errors or a grouped summary, and make that a shared component convention rather than per-form code.
## Why the raw path appears A household section on a benefits application lets the applicant add any number of dependents, each with a name and a date of birth. The rules use a **wildcard**: `'dependents.*.birth_date' => ['required', 'date', 'before:today']`. Before validating, Laravel expands the wildcard into one concrete key per submitted row (`dependents.0.birth_date`, `dependents.1.birth_date`, ...) and remembers which pattern produced them. These expanded keys are called **implicit attributes**. When a message needs `:attribute`, the validator looks for a display name for the concrete key and then for its wildcard pattern, first in the inline attributes and then in `validation.attributes`. If neither matches an implicit attribute, it returns the **raw dot path** unchanged (unless a formatter was installed with the validator's `setImplicitAttributesFormatter()`). The "underscores become spaces" fallback applies only to ordinary, non-wildcard fields. So the applicant reads "The dependents.1.birth_date field is required." ## Three ways to fix it | Approach | Entry | Resulting text | |---|---|---| | Name the wildcard | `attributes()`: `'dependents.*.birth_date' => 'date of birth'` | The date of birth field is required. | | Wildcard message | `messages()`: `'dependents.*.birth_date.required' => 'Enter a date of birth for dependent :position.'` | Enter a date of birth for dependent 2. | | Language file | `validation.custom` with a `dependents.*.birth_date` key, per locale | the locale's own sentence | The first is quick but loses **which** row failed, which matters when there are five dependents. The second and third keep the row number. All three accept `*` in the key, matching one path segment. ## Position placeholders Laravel replaces these in any custom message for an array field: 1. `:index`: the zero-based array index (`1` for the second row). 2. `:position`: the one-based position (`2` for the second row), usually what users expect. 3. `:ordinal-position`: an ordinal such as `2nd`, produced by `Number::ordinal()`; the replacement runs **only when the PHP `intl` extension is loaded**, otherwise the placeholder is left in the text. 4. `:second-index`, `:second-position`, `:third-position` and so on address deeper wildcards, as in `households.*.members.*.name`. ## Reading array errors in Blade The message bag stores each failure under its **dot path**, never under the HTML input name: - Per row: `@error("dependents.$i.birth_date")` inside a loop. - Any row: `$errors->has('dependents.*.birth_date')` is true when at least one row failed, and `first('dependents.*.birth_date')` returns the first matching message; `get()` with a wildcard returns the messages grouped by concrete key. - Mismatch trap: `@error('dependents[1][birth_date]')` never matches, because nothing in the bag uses bracket notation. The same dot paths appear as keys of the `errors` object in a 422 JSON response, so a JavaScript client must map them back to its inputs the same way. ## Nested wildcards Forms often nest one repeating group inside another, such as several households, each with several members. The rule key `households.*.members.*.name` expands to paths like `households.1.members.3.name`, and each numeric segment gets its own placeholder, counted from the left: - `:position` and `:index` refer to the **first** numeric segment (the household). - `:second-position` and `:second-index` refer to the **second** (the member). A message such as `'households.*.members.*.name.required' => 'Household :position, member :second-position: enter a name.'` therefore renders as "Household 2, member 4: enter a name." for `households.1.members.3.name`. The same wildcard key works in `attributes()` and in the language file, so the per-locale wording can use the positions too. ## Translation and pitfalls - Put per-locale row messages in `lang/{locale}/validation.php` under `custom`, keyed by the wildcard, so the Spanish form says "dependiente :position" without code changes. - An inline `messages()` string beats the language file, so a leftover English wildcard message will override the translated one. - A message using `:position` on a non-array field has nothing to replace, and the placeholder stays in the text. - Do not "fix" the display by renaming inputs to flat names like `dependent_1_birth_date`; that throws away wildcard rules and nested validated data.
- How do you show one summary line above the dependents table instead of per-row messages?`MessageBag` methods accept wildcard keys: `$errors->has('dependents.*.birth_date')` is true when any row failed, and `$errors->first('dependents.*.birth_date')` returns the first matching message. Because `@error` compiles to `has()` and `first()`, `@error('dependents.*.birth_date')` also works for a single banner.
- Why might `:ordinal-position` appear literally in a production message?The validator replaces `:ordinal-position` only when the PHP `intl` extension is loaded, since it uses `Number::ordinal()`. On a server image without `intl` the placeholder is left untouched, so either install the extension or use `:position`.
saying these in an interview costs you the question
- Wildcards are not allowed in messages() or attributes(), so every index must be listed.
- :position is zero-based, just like the array index.
- @error('dependents[1][birth_date]') matches the HTML input name.
- Laravel turns dependents.1.birth_date into "dependents 1 birth date" automatically.