Reader Stacks

How to Store Arrays and JSON in Laravel Eloquent

Use JSON plus an Eloquent cast for bounded structured data, but choose the cast deliberately, validate the shape, and normalize data that becomes relational.

For structured data that belongs to one Eloquent model, use a JSON column and an explicit cast. In Laravel 13, 'array' is the simplest cast, AsArrayObject is useful when you need to mutate individual JSON offsets directly, and AsCollection is appropriate when Collection operations are part of the model's normal workflow. Validate the JSON shape before saving it, keep mass assignment narrow, and prefer a normalized related table when nested records need independent identity, constraints, indexing, relationships, or lifecycle.

Create a JSON column

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('products', function (Blueprint $table) {
    $table->json('settings')->nullable();
});

Laravel exposes a JSON migration column type, but storage and indexing remain database-specific. Laravel's API smooths over common application patterns; it does not make MySQL, PostgreSQL, SQLite, and SQL Server JSON features identical.

Cast JSON to a PHP array

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected function casts(): array
    {
        return [
            'settings' => 'array',
        ];
    }
}

Eloquent decodes stored JSON to an array when the attribute is read and encodes an assigned array when it is persisted.

null is not automatically an empty array

Laravel documents that attributes whose value is null are not cast. Decide whether null means “not provided/unknown” and an empty array means “provided but empty”; do not collapse them accidentally if the domain distinguishes them.

Mutating a plain array cast

The ordinary array cast returns a primitive PHP array. Change a local copy and assign it back:

$product = Product::findOrFail($id);

$settings = $product->settings ?? [];
$settings['theme'] = 'dark';

$product->settings = $settings;
$product->save();

Laravel's documentation explicitly warns that direct offset mutation is not supported on the primitive array cast:

// Not supported with the plain 'array' cast:
$product->settings['theme'] = 'dark';

Use AsArrayObject for direct offset mutation

use Illuminate\Database\Eloquent\Casts\AsArrayObject;

protected function casts(): array
{
    return [
        'settings' => AsArrayObject::class,
    ];
}
$product = Product::findOrFail($id);

$product->settings['theme'] = 'dark';
$product->save();

AsArrayObject gives Laravel an object it can track/cache, so direct offset updates are supported.

Use AsCollection when Collection semantics help

use Illuminate\Database\Eloquent\Casts\AsCollection;

protected function casts(): array
{
    return [
        'tags' => AsCollection::class,
    ];
}

Choose this when the model genuinely benefits from Collection methods. If the data is simply stored and retrieved, a plain array can be easier to validate and reason about.

object and other specialized casts

protected function casts(): array
{
    return [
        'metadata' => 'object',
        'preferences' => 'array',
    ];
}

The object cast produces a stdClass. Laravel 13 also documents specialized collection/value-object casts and custom casts. Use the narrowest representation that matches the application's needs.

Update a single JSON path

$product->update([
    'settings->theme' => 'dark',
]);

Laravel supports JSON-path updates through the -> syntax where the attribute is mass assignable. This is concise, but input still needs validation and an explicit decision about which nested keys the client is allowed to control.

Validate nested JSON input

public function rules(): array
{
    return [
        'settings' => ['nullable', 'array'],
        'settings.theme' => ['sometimes', 'string', 'in:light,dark'],
        'settings.notifications' => ['sometimes', 'boolean'],
        'settings.locale' => ['sometimes', 'string', 'max:10'],
    ];
}

A cast serializes values; it does not validate or authorize them. Validation should define the allowed shape and values before persistence.

Mass assignment is a separate boundary

$data = $request->validated();

$product->fill([
    'name' => $data['name'],
    'settings' => $data['settings'] ?? null,
]);

$product->save();

Do not make permissions, billing state, ownership, moderation flags, or other server-owned values writable just because they are nested inside the same JSON document.

Query values inside JSON

$darkProducts = Product::query()
    ->where('settings->theme', 'dark')
    ->get();

$englishProducts = Product::query()
    ->whereJsonContains('settings->languages', 'en')
    ->get();

$multiLanguageProducts = Product::query()
    ->whereJsonLength('settings->languages', '>', 1)
    ->get();

Laravel 13 documents JSON path conditions and helpers such as whereJsonContains, whereJsonContainsKey, and whereJsonLength. Exact behavior and index support vary by DBMS and version; test every engine your application officially supports.

Encrypted JSON casts

protected function casts(): array
{
    return [
        'private_profile' => 'encrypted:array',
    ];
}

Laravel also supports encrypted collection/object variants. Two limits matter: encrypted ciphertext is longer and unpredictably sized, so Laravel recommends a TEXT-sized column or larger; and encrypted attributes cannot be queried/searched by plaintext contents. Normal JSON-path indexes therefore cannot make encrypted plaintext searchable.

When a normalized table is better

Use a related table when nested items have their own identifiers, foreign keys, independent create/update/delete lifecycle, row-level constraints, frequent filtering/sorting/aggregation, important indexes, or unbounded growth. Order lines, permissions, product variants, comments, and many address models are usually relational data, not merely structured metadata.

JSON is a strong fit for bounded settings, sparse metadata, third-party payload snapshots, and configuration that is normally read with the parent row.

Portability and indexing

If the application supports multiple databases, treat JSON as a portability boundary. The same Eloquent method can compile to different SQL and may have different containment, comparison, path, and index behavior. Keep database-specific assumptions in schema documentation and tests rather than hiding them in controller code.

Practical decision rule

Start with a JSON column plus 'array' for small bounded structures. Move to AsArrayObject for direct offset mutation, use AsCollection when Collection semantics help, encrypt only when you accept loss of plaintext querying, and normalize the data when it becomes relational.

Related guides

Sources and further reading

Topics: Database Queries & Eloquent