The Problem with Silent Filter Failures
Endpoints that accept a bag of options have a subtle failure mode: a client sends ?filter[stat us]=draft with a typo, your code reads $filters['status'], finds nothing, and returns the full unfiltered list. No error is raised, the response looks correct, and the bug surfaces later as an intermittent mystery.
Laravel 13.24 ships the array_keys validation rule to close this gap. It lets you declare exactly which keys an array may contain and returns a failure message that names what went wrong.
Basic Usage
Both the fluent builder and the string form are supported:
use Illuminate\Validation\Rule;
$request->validate([
'filter' => Rule::arrayKeys(['status', 'author', 'tag']),
]);
// Equivalent string form
$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. To enforce that specific keys must also be present, compose the rule with required_array_keys:
'coordinates' => [
'required_array_keys:lat,lng',
Rule::arrayKeys(['lat', 'lng']),
],
Why Not array:key_1,key_2?
Rule::array() has accepted a key list for a while, but it conflates two concerns — type checking and key checking — into one message:
| Rule | Message on unexpected key |
|---|---|
| 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 misleading when the value is an array. The new rule separates the concerns and reports them independently in $validator->failed() as Array and ArrayKeys.
Custom Messages with :unexpected
The rule ships two placeholders: :values (the allowed keys) and :unexpected (the keys that caused the failure). The :unexpected placeholder is especially useful in API responses:
$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.
Real-World Example: Filtered Index Endpoint
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.',
];
}
}
Anything that reaches the controller is a key you explicitly named, so defensive isset checks become unnecessary.
Validating a JSON Column on Writes
The rule is equally useful when persisting a settings or preferences column:
'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 silently writing a stale key into every row.
Key Behaviours to Know
- A non-array value fails the rule. Pair with
arrayso the type failure gets its own message. - At least one key is required. Passing no keys throws an
InvalidArgumentExceptionat runtime. Useprohibitedif you want to block the field entirely. - Accepts any
Arrayable. Collections and backed enums both work:Rule::arrayKeys(FilterKey::cases()). - Variadic form is supported.
Rule::arrayKeys('status', 'author')is equivalent to passing an array.
The rule was contributed by @nebarg in #60918.
Source: Reject Unexpected Array Keys with Laravel Validation — Laravel News