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
- Laravel 13.x: Eloquent Mutators & Casting
- Laravel 13.x: Query Builder
- Laravel 13.x: Validation
- Laravel 13.x: Migrations
- Laravel 13.x: Eloquent