Fixing asterisk keys in Laravel request merging

Passing a literal * key to Request::merge() could overwrite existing input instead of adding a new key.

I fixed a bug in Laravel's Request::merge(). If you passed ['*' => 226] to it, Laravel would replace every existing value at that level with 226, instead of adding a key named *.

So a request containing a name and an organisation ID could lose both values after a single merge. With an empty request, the * key wasn't added at all.

I also introduced the bug, back in PR #52242 in July 2024. That was my first-ever open-source PR. (What a start, right? 😅)

What went wrong

Suppose a request reaches your controller with this JSON body:

{
    "organisation_id": 10,
    "name": "Taylor"
}

This controller method adds a key named * and returns the resulting input. The class uses Illuminate\Http\Request and Illuminate\Http\JsonResponse:

// app/Http/Controllers/OrganisationController.php
public function store(Request $request): JsonResponse
{
    $request->merge(['*' => 226]);

    return response()->json($request->all());
}

You'd expect the existing input to stay intact, with one new key:

[
    'organisation_id' => 10,
    'name' => 'Taylor',
    '*' => 226,
]

Before the fix, the response contained this instead:

[
    'organisation_id' => 226,
    'name' => 226,
]

The asterisk was treated as “update everything here.” The original values were overwritten, and the key you wanted to add was missing. A nested key such as profile.* did the same thing to the values inside profile.

The code behind it

Inside Request::merge(), Laravel applies each incoming key to the existing input. Here is the full method from src/Illuminate/Http/Request.php before the fix:

public function merge(array $input)
{
    return tap($this, function (Request $request) use ($input) {
        $request->getInputSource()
            ->replace((new Collection($input))->reduce(
                fn ($requestInput, $value, $key) => data_set($requestInput, $key, $value),
                $this->getInputSource()->all()
            ));
    });
}

The problem is the data_set() call. It treats * as a wildcard, so assigning one value can overwrite every value at that level.

The fix

Here's the same code after PR #61309:

public function merge(array $input)
{
    return tap($this, function (Request $request) use ($input) {
        $request->getInputSource()
            ->replace((new Collection($input))->reduce(
                function ($requestInput, $value, $key) {
                    Arr::set($requestInput, $key, $value);

                    return $requestInput;
                },
                $this->getInputSource()->all()
            ));
    });
}

Arr::set() stores * as a literal key. It still supports dot notation: merging ['profile.name' => 'Otwell'] updates the name inside profile and keeps its other fields.

Why the explicit return?

Arr::set() changes its input by reference, but for a dotted key it returns the nested array it reached. A small standalone example shows the difference:

use Illuminate\Support\Arr;

$input = [
    'profile' => ['name' => 'Taylor'],
    'locale' => 'en',
];

$result = Arr::set($input, 'profile.name', 'Otwell');

// $input:  ['profile' => ['name' => 'Otwell'], 'locale' => 'en']
// $result: ['name' => 'Otwell']

The reducer passes the callback's return value into the next iteration. Returning Arr::set() directly would lose the outer structure, including locale. Returning $requestInput keeps the full input.

Regression tests

The added tests cover literal * keys, nested profile.* keys, empty input, JSON requests, and mergeIfMissing(). They also check that updating profile.name keeps the neighboring email intact.

mergeIfMissing() delegates to merge(), so it gets the same fix.

Merged into Laravel 13.x

Taylor merged #61309 into the 13.x branch on August 24, 2026.

Thanks for reading.
Hristijan

← Back to blog