0

Array validation — nested arrays, $.*

Intermediate5 min read·lv-11-005

Concept

Validating arrays — especially nested arrays of objects — requires a dedicated syntax in Laravel. The dot notation with a * wildcard tells the Validator to apply a rule to every element of an array. This is implemented in Illuminate\Validation\Validator::explodeRules(), which expands wildcard patterns against the actual data keys before the validation pass begins.

The basic pattern is 'field.*' for a flat array. For a nested array of objects, you use 'field.*.subfield'. The Validator flattens the data using Illuminate\Support\Arr::dot() to convert ['tags' => ['php', 'laravel']] into ['tags.0' => 'php', 'tags.1' => 'laravel'], then matches each key against the expanded patterns.

Declaring 'tags' => 'array' validates that tags is an array. Adding 'tags.*' => 'string|max:50' validates each element. Without the first rule, if the client sends tags as a string, the second rule would be skipped silently because * expansion yields no keys. Always validate the parent array type before validating its elements.

For deeply nested structures — common in JSON API payloads — you can nest wildcards: 'line_items.*.options.*' validates every option inside every line item. The Validator builds a complete key expansion before running rules, so error messages reference the actual path, e.g., line_items.0.options.2.

The array rule also accepts an optional list of allowed keys: 'settings' => ['array:theme,language,timezone']. This acts as a whitelist for the array's top-level keys, failing validation if any other keys are present. This is useful when accepting a structured options object where unknown keys should be rejected.

Counting array size constraints works on the parent field: 'tags' => ['array', 'min:1', 'max:5'] ensures between 1 and 5 elements. The min/max/size/between rules on arrays count elements, not sum character lengths.

Code Example

php
<?php

// Payload example:
// {
//   "order": {
//     "customer_name": "Alice",
//     "line_items": [
//       { "product_id": 1, "quantity": 2, "options": ["red", "large"] },
//       { "product_id": 5, "quantity": 1, "options": [] }
//     ]
//   },
//   "tags": ["php", "laravel"],
//   "settings": { "theme": "dark", "language": "en" }
// }

$request->validate([
    // Nested object
    'order'                          => ['required', 'array'],
    'order.customer_name'            => ['required', 'string', 'max:255'],

    // Array of objects: each line_item must have these fields
    'order.line_items'               => ['required', 'array', 'min:1'],
    'order.line_items.*.product_id'  => ['required', 'integer', 'exists:products,id'],
    'order.line_items.*.quantity'    => ['required', 'integer', 'min:1', 'max:100'],

    // Nested array inside each element
    'order.line_items.*.options'     => ['nullable', 'array', 'max:3'],
    'order.line_items.*.options.*'   => ['string', 'max:50'],

    // Flat array: 1 to 5 string tags
    'tags'                           => ['nullable', 'array', 'max:5'],
    'tags.*'                         => ['string', 'distinct', 'max:30'],

    // Array with whitelisted keys only
    'settings'                       => ['nullable', 'array:theme,language,timezone'],
    'settings.theme'                 => ['sometimes', 'string', 'in:light,dark,system'],
    'settings.language'              => ['sometimes', 'string', 'size:2'],
]);

// Accessing errors for array fields
// $errors->get('order.line_items.0.product_id') => ['The product id field is required.']
// $errors->get('tags.2') => ['The tags.2 must not be greater than 30 characters.']

Interview Q&A

Q: How does Laravel's Validator expand wildcard rules for nested arrays, and what happens if a parent array is empty?

The Validator calls Arr::dot() on the input data to produce a flat key-value map (e.g., order.line_items.0.product_id). Wildcard patterns like order.line_items.*.product_id are matched against this flattened map. If order.line_items is an empty array, Arr::dot() produces no keys matching the wildcard, so the child rules are never evaluated. This is why you must always pair a min:1 rule on the parent array when at least one element is required — you cannot rely on child rules to catch an empty array.


Q: What does the distinct rule do, and when is it important for array validation?

The distinct rule checks that the field's value does not appear more than once within the same array level. For tags.*, it ensures no duplicate tag values are submitted. By default the comparison is case-sensitive; you can use distinct:ignore_case to make it case-insensitive. This is important for fields that map to database unique constraints (e.g., a list of email addresses to invite) — catching duplicates at the validation layer avoids a confusing unique-constraint violation at the database layer.


Q: How do validation error messages reference specific array elements, and how do you customise them?

The Validator generates error keys using the exact dot-notation path of the failing element, such as order.line_items.2.quantity. In Blade templates, $errors->get('order.line_items.*') uses a wildcard to retrieve all errors under that path. To customise messages for array fields, use the element wildcard in the messages() method of a FormRequest or the fourth argument to Validator::make(): 'order.line_items.*.product_id.exists' => 'One of the products you selected does not exist.'. The attribute names can likewise be customised to produce friendlier labels.