News

Reject Unexpected Array Keys with Laravel Validation

Published
Reject Unexpected Array Keys with Laravel Validation image

Endpoints that take a bag of options have a quiet failure mode. A client sends ?filter[stat us]=draft with a typo, your code reads $filters['status'], finds nothing, and returns the unfiltered list. Nobody gets an error, the response looks fine, and the bug surfaces later as "the filter doesn't work sometimes."

Laravel 13.24 adds an array_keys validation rule for exactly this: an array may contain these keys and nothing else, with a failure message that names what went wrong.

The Rule

Both the builder and the string form work:

use Illuminate\Validation\Rule;
 
$request->validate([
'filter' => Rule::arrayKeys(['status', 'author', 'tag']),
]);
 
// Equivalent
$request->validate([
'filter' => 'array_keys:status,author,tag',
]);

Given ['status' => 'draft', 'stat us' => 'draft'], validation fails with:

The filter field must only contain the following keys: status, author, tag.

The keys are permitted, not required. The rule constrains what may appear; it does not demand that anything be present. If you need the other half, required_array_keys still covers it and the two compose:

'coordinates' => [
'required_array_keys:lat,lng',
Rule::arrayKeys(['lat', 'lng']),
],

That pair reads as "exactly these keys, no more and no fewer."

Why Not array:key_1,key_2?

Rule::array() has accepted a key list for a while, and it does reject unexpected keys. The difference is what happens when it fails. The array rule answers two questions, "is this an array?" and "does it only have these keys?", and reports both with the same message:

Rule Message on ['status' => 'draft', 'colour' => 'red']
array:status,author The filter field must be an array.
array_keys:status,author The filter field must only contain the following keys: status, author.

The first message is actively misleading, since the value is an array. There is also no placeholder for the offending keys, so you cannot write a custom message that names them.

The two rules report separately in $validator->failed(), as Array and ArrayKeys, so you can apply both when you want each failure distinguishable:

'filter' => ['array', Rule::arrayKeys(['status', 'author', 'tag'])],

Rule::array() and its message are unchanged, so nothing you have already written behaves differently.

Naming the Offending Keys

The rule ships two placeholders. :values holds the accepted keys and is what the default message uses. :unexpected holds the keys that actually caused the failure, and while it is not in the default message, it is usually the more useful half in an API response:

$request->validate(
['filter' => Rule::arrayKeys(['status', 'author', 'tag'])],
['filter.array_keys' => 'The :attribute field may not contain :unexpected.'],
);
 
// The filter field may not contain colour, sort.

Telling a client which key it got wrong turns a guessing game into a one-line fix, which is worth the custom message on a public API.

A Filtered Index Endpoint

Putting it together on a route that takes filters, sorting, and includes, each with its own allowed key set:

namespace App\Http\Requests;
 
use App\Enums\PostStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
 
class IndexPostRequest extends FormRequest
{
public function rules(): array
{
return [
'filter' => ['sometimes', 'array', Rule::arrayKeys(['status', 'author', 'tag'])],
'filter.status' => ['sometimes', Rule::enum(PostStatus::class)],
'filter.author' => ['sometimes', 'integer', 'exists:users,id'],
'filter.tag' => ['sometimes', 'string', 'max:50'],
 
'sort' => ['sometimes', 'string', Rule::in(['title', '-title', 'published_at', '-published_at'])],
];
}
 
public function messages(): array
{
return [
'filter.array_keys' => 'Unknown filter: :unexpected. Allowed filters are :values.',
];
}
}

The controller can then read the filter bag without defensive checks, because anything that reached it is a key you named:

public function index(IndexPostRequest $request)
{
$filters = $request->validated('filter', []);
 
return PostResource::collection(
Post::query()
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
->when($filters['author'] ?? null, fn ($q, $author) => $q->where('user_id', $author))
->when($filters['tag'] ?? null, fn ($q, $tag) => $q->whereRelation('tags', 'slug', $tag))
->paginate()
);
}

Without the rule, ?filter[autor]=3 returns every post and looks like a working request. With it, the client gets a 422 naming autor.

Validating a JSON Column

The same rule is useful on writes, where a settings or preferences column tends to accumulate whatever the frontend happened to send:

'preferences' => ['sometimes', 'array', Rule::arrayKeys(['theme', 'timezone', 'digest_frequency'])],
'preferences.theme' => ['sometimes', Rule::in(['light', 'dark', 'system'])],
'preferences.timezone' => ['sometimes', 'timezone'],
'preferences.digest_frequency' => ['sometimes', Rule::in(['daily', 'weekly', 'never'])],

A renamed frontend field now fails loudly during deployment instead of writing a key nothing reads into every row.

Details Worth Knowing

A few behaviors that are not obvious from the message:

  • A non-array value fails the rule. Passing 'filter' => 'draft' produces the "must only contain the following keys" message, which reads oddly for a string. Pair it with array when the input might not be an array at all, so the type failure is reported on its own terms.

  • The rule needs at least one key. Writing 'array_keys' with no keys raises an InvalidArgumentException when validation runs rather than failing the field. There is no "allow nothing" form; prohibited covers that case.

  • Keys can come from anywhere Arrayable. A collection or an enum-backed list works, and backed enums are resolved to their values:

    Rule::arrayKeys(FilterKey::cases());
    Rule::arrayKeys(collect(config('filters.allowed')));
  • The variadic form works too. Rule::arrayKeys('status', 'author') is equivalent to passing the array, which is handy inline.

The rule was contributed by @nebarg in #60918.

Further Reading

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Filed in

Sponsored

masteringlaravel logo
Laravel Code Review

Get expert guidance in a few days with a Laravel code review

Visit Laravel Code Review

The latest

View all →
Validate and Convert HEIC Images in Laravel image

Validate and Convert HEIC Images in Laravel

Read article
Saga Lara Flow: Durable Workflows and Compensating Transactions on Laravel Queues image

Saga Lara Flow: Durable Workflows and Compensating Transactions on Laravel Queues

Read article
Major performance improvements & security patches for Filament v4.12 and v5.7! image

Major performance improvements & security patches for Filament v4.12 and v5.7!

Read article
Laravel Boost Project Rules: Teach Agents Your Conventions image

Laravel Boost Project Rules: Teach Agents Your Conventions

Read article
Extract an Image's Dominant Color in Laravel image

Extract an Image's Dominant Color in Laravel

Read article
Image Dominant Color and HEIC Support in Laravel 13.24 image

Image Dominant Color and HEIC Support in Laravel 13.24

Read article