Валидация массивов и JSON в Laravel
HTML-формы отправляют не только плоские поля. Мультиселекты, динамические блоки «добавить ещё», JSON-тела API-запросов – всё это массивы и вложенные структуры. Laravel валидирует их через dot notation и wildcard *, не требуя ручного обхода элементов. Правила для массивов покрывают и структуру (какие ключи, сколько элементов), и содержимое (тип, формат, существование в базе).
Правило array
Правило array проверяет, что поле является PHP-массивом. Без него Laravel воспримет строку "tags" как валидное значение для поля, которое ожидает список:
$request->validate([
'tags' => 'required|array',
'tags.*' => 'string|max:50',
]);
Ограничение допустимых ключей
Если передать аргументы, array проверит, что массив содержит только перечисленные ключи:
$request->validate([
'address' => 'required|array:street,city,zip,country',
]);
Ключ state, отправленный клиентом, вызовет ошибку. Это защита от лишних полей – особенно при $model->fill($validated). Без ограничения ключей validated() вернёт все поля массива, даже непровалидированные.
array vs list
array допускает и списки ([1, 2, 3]), и ассоциативные массивы (['a' => 1]). Если нужен именно последовательный список, добавьте правило list (подробнее ниже).
При валидации API-тел, где клиент отправляет JSON-объект, array – основной инструмент. Для HTML-форм с name="items[]" данные тоже приходят как PHP-массив.
Количество элементов и пустые массивы
Для проверки длины массива используйте min, max, size и between:
$request->validate([
'recipients' => 'required|array|min:1|max:50',
'options' => 'required|array|size:3',
'scores' => 'required|array|between:2,10',
]);
Правило size для массивов проверяет count(). min:1 гарантирует непустой массив. between:2,10 – от 2 до 10 элементов включительно. Эти правила работают одинаково для списков и ассоциативных массивов – считают количество элементов верхнего уровня.
Если пустой массив допустим:
'filters' => 'present|array',
present требует наличие ключа в запросе, но разрешает пустой массив []. required|array отклонит пустой массив, потому что пустой массив считается «пустым значением».
Для полностью необязательного массива (может отсутствовать или быть пустым):
'tags' => 'nullable|array',
'tags.*' => 'string|max:50',
При nullable поле может быть null (отсутствует). При его наличии – должен быть массивом. Подробнее о разнице nullable, sometimes и present – в статье про условную валидацию.
Wildcard * и dot notation
Символ * заменяет любой индекс массива. Dot notation обращается к вложенным ключам:
$request->validate([
'users' => 'required|array|min:1',
'users.*.name' => 'required|string|max:255',
'users.*.email' => 'required|email',
'users.*.role' => 'required|in:admin,editor,viewer',
]);
Для каждого элемента users Laravel проверит name, email и role. Количество элементов не ограничено (кроме min:1). Добавьте max, если нужен лимит – это и защита от абьюза, и сигнал для фронтенда.
Вложенность может быть глубже одного уровня:
$request->validate([
'order.items' => 'required|array|min:1',
'order.items.*.product_id' => 'required|integer|exists:products,id',
'order.items.*.quantity' => 'required|integer|min:1',
'order.items.*.options' => 'nullable|array',
'order.items.*.options.*.name' => 'required|string',
'order.items.*.options.*.value' => 'required|string',
]);
Два уровня wildcard: каждый товар имеет массив опций, у каждой опции – имя и значение. Laravel не ограничивает глубину вложенности – можно добавить третий, четвёртый уровень *, если структура данных это требует.
Dot notation без wildcard
Wildcard не обязателен. Для известных ключей обращайтесь напрямую:
$request->validate([
'meta.title' => 'required|string|max:60',
'meta.description' => 'nullable|string|max:160',
'meta.og_image' => 'nullable|url',
]);
Это типичная ситуация для объектов с фиксированной структурой – SEO-мета, настройки, конфигурация.
Dot notation для конкретных элементов
Можно обратиться к конкретному индексу вместо wildcard:
$request->validate([
'contacts.0.phone' => 'required|string',
'contacts.1.phone' => 'nullable|string',
]);
Первый контакт обязателен, второй – нет. Этот подход полезен для форм с фиксированной структурой: форма оплаты с основным и дополнительным адресом, форма регистрации с главным и запасным контактом.
Комбинация wildcard и фиксированных ключей
В реальных формах уровни вложенности чередуются: массив объектов, у каждого – известные ключи:
$request->validate([
'employees' => 'required|list|min:1',
'employees.*.personal' => 'required|array:first_name,last_name,birth_date',
'employees.*.personal.first_name' => 'required|string|max:100',
'employees.*.personal.last_name' => 'required|string|max:100',
'employees.*.personal.birth_date' => 'required|date|before:today',
'employees.*.contacts' => 'required|array|min:1',
'employees.*.contacts.*.type' => 'required|in:phone,email',
'employees.*.contacts.*.value' => 'required|string',
]);
Первый * – индекс сотрудника, personal – объект с ограниченными ключами, contacts – вложенный список, второй * – индекс контакта.
Экранирование точек в именах полей
Если имя поля содержит буквальную точку (например, v1.0), Laravel интерпретирует её как разделитель вложенности. Экранируйте обратным слешем:
$request->validate([
'config.v1\.0' => 'required|string',
]);
Без экранирования Laravel будет искать ключ 0 внутри ключа v1 внутри config.
Wildcard в сообщениях об ошибках
Ошибки для wildcard-правил содержат конкретный индекс. Если users.2.email невалиден, ключ ошибки будет users.2.email, а не users.*.email. Это позволяет фронтенду точно определить, какой элемент формы подсветить красным.
В JSON-ответе валидации (422 Unprocessable Entity) ошибки приходят в формате:
{
"message": "The users.2.email field must be a valid email address.",
"errors": {
"users.2.email": [
"The users.2.email field must be a valid email address."
]
}
}
JavaScript-клиент может распарсить ключ users.2.email и определить индекс 2 из dot-нотации.
Массив целых чисел, строк, boolean
Типичная задача – убедиться, что каждый элемент массива определённого типа:
$request->validate([
'category_ids' => 'required|array|min:1',
'category_ids.*' => 'integer|min:1',
'permissions' => 'required|array',
'permissions.*' => 'string|in:read,write,delete',
'features' => 'required|array',
'features.*' => 'boolean',
]);
category_ids.* = каждый элемент должен быть целым числом. permissions.* = каждый элемент – строка из допустимого набора. features.* = каждый элемент true/false.
Когда поле может быть строкой или массивом (фильтр API, который принимает одно значение или список), валидация усложняется. Самый надёжный подход – нормализовать в prepareForValidation():
protected function prepareForValidation(): void
{
$status = $this->input('status');
if (is_string($status)) {
$this->merge(['status' => [$status]]);
}
}
public function rules(): array
{
return [
'status' => 'required|array|min:1',
'status.*' => 'string|in:active,archived,draft',
];
}
После нормализации status всегда массив – правила становятся однозначными. Подход через prepareForValidation() описан подробнее в статье про Form Request.
Ключи массива: array keys, required_array_keys
required_array_keys – обязательные ключи
Проверка, что массив содержит определённые ключи:
$request->validate([
'config' => 'required|array|required_array_keys:host,port,database',
]);
config должен содержать ключи host, port и database. Дополнительные ключи допустимы. Для запрета лишних ключей комбинируйте с array:host,port,database.
in_array_keys – хотя бы один из ключей
Когда нужно убедиться, что массив содержит хотя бы один ключ из набора:
$request->validate([
'notification' => 'required|array|in_array_keys:email,sms,push',
]);
Массив notification должен содержать хотя бы один из перечисленных ключей. Если ни одного из ключей нет – ошибка. Это полезно для форм, где пользователь выбирает способ уведомления – хотя бы один канал обязателен.
Проверка наличия конкретных ключей в любом порядке
Комбинация required_array_keys + array:keys даёт полный контроль над структурой:
$request->validate([
'database' => [
'required',
'array:host,port,name,user,password',
'required_array_keys:host,port,name',
],
]);
Ключи host, port, name обязательны. user и password опциональны, но других ключей быть не может. Такой подход защищает конфигурационные объекты от лишних полей.
Валидация значений по ключам
Когда ключи фиксированные, правила для каждого задаются через dot notation:
$request->validate([
'dimensions' => 'required|array|required_array_keys:width,height',
'dimensions.width' => 'required|integer|min:1|max:4096',
'dimensions.height' => 'required|integer|min:1|max:4096',
]);
required_array_keys гарантирует наличие ключей, правила с dot notation проверяют значения. Это два слоя защиты: структура + содержимое.
in_array – значение существует в другом поле
in_array проверяет, что значение присутствует среди значений другого поля:
$request->validate([
'available_colors' => 'required|array',
'available_colors.*' => 'string',
'default_color' => 'required|in_array:available_colors.*',
]);
Цвет по умолчанию должен быть одним из доступных цветов. Валидация сравнивает значения внутри запроса, а не в базе. Для проверки по базе используйте exists – об этом подробно в статье про unique и exists.
Обратный вариант – проверить, что значение не среди значений другого поля – Laravel не предоставляет из коробки. Если нужно, создайте собственное правило валидации.
list – массив-список
list гарантирует, что массив является списком – ключи идут последовательно от 0:
$request->validate([
'steps' => 'required|list|min:2',
'steps.*' => 'required|string|max:500',
]);
Ассоциативный массив ['a' => 1, 'b' => 2] не пройдёт – только [1, 2, 3]. Это важно для данных, где порядок имеет значение (шаги мастера, очередь задач). PHP-функция array_is_list() использует ту же логику.
list сочетается с min, max, size:
'steps' => 'required|list|between:3,10',
Если клиент отправит {"0": "a", "2": "b"} (пропущен индекс 1), list отклонит данные – ключи не последовательные.
contains и doesnt_contain
Проверка наличия или отсутствия конкретных значений в массиве:
use Illuminate\Validation\Rule;
$request->validate([
'roles' => [
'required',
'array',
Rule::contains(['admin']),
],
]);
Массив roles обязан содержать значение admin. Другие значения допустимы.
'tags' => [
'required',
'array',
Rule::doesntContain(['banned', 'spam']),
],
Массив tags не должен содержать banned или spam. 'admin' и 'Admin' считаются разными значениями – регистр учитывается.
contains и doesnt_contain проверяют наличие/отсутствие среди значений массива, а не среди ключей. Для проверки ключей используйте required_array_keys или in_array_keys.
distinct – уникальность элементов
distinct проверяет, что внутри массива нет дубликатов:
$request->validate([
'assignees' => 'required|array|min:1',
'assignees.*' => 'integer|distinct',
]);
Отправка [3, 7, 3] провалит валидацию – ID 3 повторяется. По умолчанию сравнение нестрогое. Варианты:
'items.*.sku' => 'string|distinct:strict', // строгое сравнение типов
'emails.*' => 'email|distinct:ignore_case', // регистронезависимое
distinct:ignore_case полезен для email – [email protected] и [email protected] будут считаться дубликатами.
distinct проверяет уникальность внутри одного запроса. Для проверки по базе данных (email ещё не зарегистрирован) нужен unique – это другое правило с другой задачей. Часто нужны оба:
'emails.*' => 'email|distinct:ignore_case|unique:users,email',
distinct – нет дубликатов в массиве. unique – нет совпадений с базой. О правилах валидации паролей и подтверждении (confirmed, same) – в статье про валидацию паролей.
Rule::forEach – динамические правила для элементов
Rule::forEach() позволяет назначить правила каждому элементу массива в зависимости от его значения. Замыкание получает значение и полный путь атрибута, возвращает плоский массив правил:
use App\Rules\HasPermission;
use Illuminate\Validation\Rule;
$request->validate([
'company_ids' => 'required|array|min:1',
'company_ids.*' => Rule::forEach(function (string|null $value, string $attribute) {
return [
'integer',
Rule::exists('companies', 'id'),
new HasPermission('manage-company', $value),
];
}),
]);
Каждый ID компании проверяется на существование в базе и на наличие у пользователя прав на управление этой конкретной компанией. Замыкание возвращает плоский массив правил – как если бы вы писали 'company_ids.*' => ['integer', ...], только с доступом к значению через $value.
$attribute содержит путь с индексом, например company_ids.0, company_ids.1. Через $value доступно значение текущего элемента.
Для условной валидации внутри элементов-объектов (разные правила в зависимости от type или age) используйте $validator->sometimes() с двумя аргументами в замыкании – это описано в статье про условную валидацию.
Валидация JSON-строк
Правило json
json проверяет, что значение является валидной JSON-строкой:
$request->validate([
'metadata' => 'required|json',
]);
Поле metadata должно содержать строку, которую json_decode() обработает без ошибок. Пустая строка, число или массив не пройдут – нужна именно строка формата JSON. Правило не делает различий между JSON-объектом "{}", JSON-массивом "[]" и JSON-скаляром "42" – все считаются валидным JSON.
Валидация содержимого JSON
Правило json проверяет только формат, но не структуру. Для проверки содержимого распарсите JSON в prepareForValidation() и валидируйте как массив:
use Illuminate\Foundation\Http\FormRequest;
class ImportRequest extends FormRequest
{
protected function prepareForValidation(): void
{
if (is_string($this->input('payload'))) {
$decoded = json_decode($this->input('payload'), true);
if (is_array($decoded)) {
$this->merge(['payload' => $decoded]);
}
}
}
public function rules(): array
{
return [
'payload' => 'required|array',
'payload.type' => 'required|string|in:import,sync',
'payload.items' => 'required|array|min:1',
'payload.items.*.id' => 'required|integer',
'payload.items.*.data' => 'required|array',
];
}
}
Клиент отправляет JSON-строку, prepareForValidation() декодирует её, дальше валидация работает как с обычным массивом. Это единственный надёжный способ проверить «JSON-схему» в Laravel – встроенного правила json_schema нет.
Если JSON невалиден, json_decode() вернёт null, и $this->merge() не сработает. Валидация 'payload' => 'required|array' провалится с понятной ошибкой – поле осталось строкой.
JSON-массив vs JSON-объект
API-клиенты иногда отправляют JSON-массив [{...}, {...}] вместо объекта {"items": [...]}. Для валидации корневого JSON-массива декодируйте его и оберните:
protected function prepareForValidation(): void
{
$body = $this->getContent();
$decoded = json_decode($body, true);
if (is_array($decoded) && array_is_list($decoded)) {
$this->merge(['items' => $decoded]);
}
}
public function rules(): array
{
return [
'items' => 'required|list|min:1|max:100',
'items.*.id' => 'required|integer',
'items.*.action' => 'required|in:create,update,delete',
];
}
array_is_list() проверяет, что это именно список, а не ассоциативный массив.
Для JSON-объекта, приходящего как тело запроса (не строка в поле), Laravel декодирует его автоматически. Правило json не нужно – данные уже массив:
// Запрос: POST /api/events с Content-Type: application/json
// Тело: {"title": "Conf", "date": "2026-05-01"}
// В контроллере: 'Conf' === $request->input('title')
$request->validate([
'title' => 'required|string',
'date' => 'required|date',
]);
Различие: json – для строковых полей формы. Для JSON API-тел – обычные правила массивов и скаляров. Если клиент отправляет Content-Type: application/json с телом {"items": [...]}, то $request->input('items') уже будет PHP-массивом – декодирование происходит автоматически в middleware Laravel.
Частая ошибка при работе с JSON API – добавить правило json для поля, которое уже декодировано. Валидация провалится, потому что поле содержит массив, а не строку.
Сообщения об ошибках для массивов
Wildcard работает и в кастомных сообщениях. Placeholders :index (с 0), :position (с 1) и :ordinal-position (1st, 2nd… – вывод всегда на английском независимо от локали) подставляют номер элемента:
public function messages(): array
{
return [
'users.*.email.required' => 'Email пользователя #:position обязателен.',
'users.*.email.email' => 'Email пользователя #:position некорректен.',
'items.*.product_id.exists' => 'Товар в строке #:position не найден.',
];
}
Для многоуровневой вложенности: second-index, second-position, third-index и так далее:
'order.items.*.options.*.value.required' => 'Опция #:second-position товара #:position не заполнена.',
Вложенные объекты: ключ-значение
Формы часто отправляют ассоциативные массивы – настройки, метаданные, переводы:
$request->validate([
'settings' => 'required|array:theme,locale,notifications',
'settings.theme' => 'required|string|in:light,dark,auto',
'settings.locale' => 'required|string|size:2',
'settings.notifications' => 'required|array',
'settings.notifications.email' => 'required|boolean',
'settings.notifications.sms' => 'required|boolean',
]);
Здесь settings – не список элементов, а объект с известными ключами. array:theme,locale,notifications запрещает клиенту добавить ключи вроде settings.admin.
Для произвольных пар ключ-значение (переводы, мета-поля):
$request->validate([
'translations' => 'required|array|min:1',
'translations.*' => 'required|string|max:5000',
]);
Ключи массива translations – коды языков, значения – тексты. Wildcard * покрывает все ключи, какими бы они ни были.
Для валидации самих ключей можно использовать кастомное правило или after():
public function after(): array
{
return [
function (\Illuminate\Validation\Validator $validator) {
$validLocales = ['en', 'ru', 'de', 'fr', 'es', 'zh'];
$keys = array_keys($this->input('translations', []));
foreach ($keys as $key) {
if (false === in_array($key, $validLocales, true)) {
$validator->errors()->add(
"translations.{$key}",
"Неизвестный код языка: {$key}"
);
}
}
},
];
}
Стандартные правила валидируют значения, но не ключи. after() закрывает этот пробел. Как работает after() и другие пост-валидационные хуки – в статье про условную валидацию.
Ещё один вариант – разрешить только определённые ключи через array:en,ru,de,fr,es,zh и валидировать значения через wildcard:
$request->validate([
'translations' => 'required|array:en,ru,de,fr,es,zh|min:1',
'translations.*' => 'required|string|max:5000',
]);
Проще, чем after(), но ограничивает набор ключей жёстко. Подход с after() гибче, если языки хранятся в конфиге.
Полный пример: форма с динамическими блоками
Форма создания мероприятия с участниками и расписанием – несколько уровней вложенности и разные типы данных:
class CreateEventRequest extends FormRequest
{
public function rules(): array
{
return [
// Основные поля
'title' => 'required|string|max:255',
'date' => 'required|date|after:today',
// Участники: список объектов
'participants' => 'required|list|min:1|max:200',
'participants.*.name' => 'required|string|max:255',
'participants.*.email' => 'required|email|distinct:ignore_case',
'participants.*.role' => 'required|in:speaker,attendee,organizer',
// Расписание: ассоциативный массив по слотам
'schedule' => 'required|array|min:1',
'schedule.*.time' => 'required|date_format:H:i',
'schedule.*.title' => 'required|string|max:100',
'schedule.*.speaker_email' => [
'nullable',
'email',
'in_array:participants.*.email',
],
// Метаданные: объект с фиксированными ключами
'meta' => 'required|array:location,capacity,is_online',
'meta.location' => 'required_unless:meta.is_online,true|string|max:500',
'meta.capacity' => 'required|integer|min:1|max:10000',
'meta.is_online' => 'required|boolean',
// Теги: простой список строк
'tags' => 'nullable|array|max:10',
'tags.*' => 'string|max:30|distinct:ignore_case',
];
}
public function messages(): array
{
return [
'participants.*.email.distinct' => 'Email участника #:position дублируется.',
'schedule.*.speaker_email.in_array' => 'Спикер в слоте #:position не найден среди участников.',
];
}
}
Здесь комбинируются list (участники – упорядоченный список), array с ограниченными ключами (мета), in_array (спикер должен быть среди участников), distinct (email не дублируются), условная валидация (локация не нужна для онлайн-мероприятия).
Производительность при валидации массивов
Каждое правило с wildcard * применяется к каждому элементу. Для массива из 100 элементов с тремя правилами на элемент – 300 проверок. Сами по себе правила формата (string, integer, email) лёгкие. Тяжёлые – exists и unique, потому что генерируют SQL-запросы.
Для больших массивов с проверкой по БД используйте подход из статьи про unique и exists: поставьте array и exists на родительское поле для группировки в один WHERE IN, или вынесите проверку в after() с одним ручным запросом.
Ограничивайте максимальный размер массивов через max:
'items' => 'required|array|max:500',
Без max клиент может отправить массив из 100 000 элементов и нагрузить валидацию. Это особенно критично для публичных API без аутентификации.
Правила формата (string, integer, in) работают быстро – десятки тысяч проверок за миллисекунды. Узкие места – правила с обращением к БД и к внешним сервисам.
Для отладки количества запросов при валидации массивов:
DB::enableQueryLog();
$request->validate([...]);
$queries = DB::getQueryLog();
// Посмотреть, сколько SQL сгенерировала валидация
Массивы файлов
HTML-формы с <input type="file" name="photos[]" multiple> отправляют массив файлов. Валидация работает через тот же wildcard:
$request->validate([
'photos' => 'required|array|min:1|max:10',
'photos.*' => 'image|max:5120',
]);
Каждый файл проверяется отдельно – тип и размер. Для разных слотов с разными правилами:
$request->validate([
'documents' => 'required|array',
'documents.*.file' => 'required|file|mimes:pdf,docx|max:10240',
'documents.*.label' => 'required|string|max:100',
]);
Каждый документ – объект с файлом и подписью. Подробнее о валидации файлов и изображений – в статье про файловую валидацию.
Массивы и validated()
Метод validated() возвращает только провалидированные данные. Для массивов есть нюанс: если правила описаны только для users.*.email, но не для users.*.name, то name всё равно попадёт в validated(), потому что весь элемент массива считается «провалидированным».
// Правила
'users' => 'required|array',
'users.*.email' => 'required|email',
// Данные: [['email' => '[email protected]', 'name' => 'attacker', 'is_admin' => true]]
// validated() вернёт ВСЁ, включая is_admin
Защита – ограничить ключи:
'users' => 'required|array',
'users.*' => 'array:email,name',
'users.*.email' => 'required|email',
'users.*.name' => 'required|string|max:255',
Теперь is_admin не пройдёт через array:email,name. Другой вариант – использовать $request->safe()->only(['users']) и вручную фильтровать структуру. Метод safe() возвращает объект ValidatedInput, который поддерживает only(), except() и all() для гибкого доступа к провалидированным данным. При работе с fill() моделей этот контроль критичен – лишние поля не должны попадать в массовое присвоение.
Типичные ошибки
required|array и пустой массив. required|array отклонит пустой массив [] – пустой массив считается «пустым значением». Но min:1 делает намерение явным для читателя кода и защищает от путаницы.
Пропущенные правила для вложенных полей. Если объявить 'users' => 'array' без 'users.*.email' => 'required|email', Laravel не проверит содержимое элементов. Данные попадут в validated() без валидации. Это одна из самых коварных проблем – код работает, тесты зелёные, но невалидные данные проскальзывают в базу. Всегда описывайте правила для каждого уровня вложенности.
Лишние ключи в validated(). Без ограничения array:key1,key2 метод validated() вернёт все ключи массива, даже если правила описаны только для части из них. Клиент может подсунуть is_admin: true, и оно пройдёт в fill().
Wildcard и строковые поля. Правило 'tags.*' => 'string' не сработает, если tags – строка, а не массив. Добавьте 'tags' => 'array' – оно проверит тип и при провале остановит дальнейшую валидацию вложенных полей. Без него wildcard-правило просто пропустится, и невалидные данные попадут в validated().
Ассоциативный массив вместо списка. JavaScript отправляет {0: 'a', 1: 'b'} как объект, не как массив. PHP декодирует это в ассоциативный массив ['0' => 'a', '1' => 'b'], который пройдёт array, но не list. Если порядок элементов важен, всегда добавляйте list.
distinct без ignore_case для email. distinct по умолчанию чувствителен к регистру. [email protected] и [email protected] не будут считаться дубликатами. Добавляйте distinct:ignore_case или нормализуйте данные перед валидацией.
JSON как string, не как array. Если API принимает JSON-тело через Content-Type: application/json, Laravel автоматически декодирует его – поле уже приходит массивом. Правило json нужно только для полей, где JSON передаётся как строка внутри формы (textarea, hidden input).
Массив или объект в JSON. JavaScript-клиенты отправляют [] как пустой массив, а {} как пустой объект. PHP декодирует оба в массив, но [] будет пустым списком, а {} – пустым ассоциативным массивом. Для Laravel оба пройдут array, но list примет только [].
Тестирование
Массивы и вложенные структуры требуют тестов с разными формами данных:
use Tests\TestCase;
class OrderValidationTest extends TestCase
{
public function test_order_requires_at_least_one_item(): void
{
$response = $this->postJson('/api/orders', [
'items' => [],
]);
$response->assertJsonValidationErrors(['items']);
}
public function test_each_item_validated_independently(): void
{
$response = $this->postJson('/api/orders', [
'items' => [
['product_id' => 1, 'quantity' => 5],
['product_id' => null, 'quantity' => -1],
],
]);
$response->assertJsonValidationErrors([
'items.1.product_id',
'items.1.quantity',
]);
$response->assertJsonMissingValidationErrors([
'items.0.product_id',
'items.0.quantity',
]);
}
public function test_extra_array_keys_rejected(): void
{
$response = $this->postJson('/api/settings', [
'settings' => [
'theme' => 'dark',
'locale' => 'ru',
'is_admin' => true,
],
]);
$response->assertJsonValidationErrors(['settings']);
}
public function test_json_string_decoded_and_validated(): void
{
$payload = json_encode([
'type' => 'import',
'items' => [['id' => 1, 'data' => ['name' => 'test']]],
]);
$response = $this->postJson('/api/import', [
'payload' => $payload,
]);
$response->assertJsonMissingValidationErrors(['payload']);
}
public function test_distinct_catches_duplicate_ids(): void
{
$response = $this->postJson('/api/teams', [
'member_ids' => [5, 12, 5],
]);
$response->assertJsonValidationErrors(['member_ids.2']);
}
}
Во втором тесте ошибки привязаны к конкретному индексу (items.1.product_id), а не к массиву целиком. Это позволяет фронтенду подсветить конкретную строку формы.
При тестировании JSON-строк передавайте данные через postJson() – метод автоматически устанавливает Content-Type: application/json. Для тестирования поля с JSON-строкой внутри формы используйте post() с массивом данных, где JSON – обычная строка.
Массивы с distinct тестируйте парами: отправьте массив без дубликатов (должен пройти) и с дубликатами (конкретный индекс должен провалиться). Laravel помечает ошибкой все элементы-дубликаты, а не только последний. Для list – отправьте ассоциативный массив и убедитесь, что валидация его отклоняет.
Условная валидация массивов (правила зависят от значения внутри элемента) разобрана в статье про условную валидацию. Проверка существования элементов в базе – в статье про unique и exists. О правилах для отдельных полей внутри массива (строки, числа, даты) – в соответствующих руководствах: строки и числа, даты. Основы валидации – в руководстве по валидации.