To store JSON data in a database with Laravel 13, add a json column in the migration, cast it to array (or AsArrayObject) in the Eloquent model, and read or write it like a normal PHP array. You can also filter on keys inside the JSON with the arrow syntax. This guide targets Laravel 13 and PHP 8.3 or newer.
When should you store JSON instead of columns?
Use a JSON column for flexible, loosely structured data: user preferences, feature flags, form builder answers, or metadata that differs per record. Use normal columns for data you filter, sort, join or constrain often. A JSON column cannot enforce foreign keys, and heavy querying inside it is slower than a proper indexed column.
Prerequisites
- Laravel 13 on PHP 8.3 or newer.
- MySQL 8, MariaDB, PostgreSQL or SQLite. All support JSON columns in current versions.
Step 1: Add the migration
php artisan make:model Product -m<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->json('options')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};$table->json() creates a native JSON type where the database has one. Use jsonb() on PostgreSQL if you want binary storage and indexing support.
Step 2: How do you cast the column in the model?
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'options'];
protected function casts(): array
{
return [
'options' => 'array',
];
}
}With the array cast, Laravel encodes the array to JSON on save and decodes it on read. Laravel 13 models define casts in the casts() method.
Step 3: Save and read JSON data
$product = Product::create([
'name' => 'Trail Backpack',
'options' => [
'color' => 'green',
'sizes' => ['M', 'L'],
'specs' => ['weight_kg' => 1.2, 'waterproof' => true],
],
]);
$product->options['color']; // "green"
$product->options['specs']['weight_kg']; // 1.2There is one common surprise. You cannot modify a single key on an array-cast attribute directly, because PHP returns a copy. This code fails with an "Indirect modification" notice:
$product->options['color'] = 'blue'; // does not workRead the array, change it, and assign it back:
$options = $product->options;
$options['color'] = 'blue';
$product->options = $options;
$product->save();Alternatively, cast the column to Illuminate\Database\Eloquent\Casts\AsArrayObject. It returns an object that tracks changes, so $product->options['color'] = 'blue' works and save() persists it.
Step 4: How do you query inside a JSON column?
Product::where('options->color', 'green')->get();
Product::where('options->specs->waterproof', true)->get();
Product::whereJsonContains('options->sizes', 'L')->get();
Product::whereJsonLength('options->sizes', '>', 1)->get();Use the arrow to reach nested keys. whereJsonContains checks if an array holds a value, and whereJsonLength compares the number of items. You can update one key without loading the model:
Product::whereKey($product->id)->update(['options->color' => 'red']);That form works on databases with JSON support, and Laravel builds the correct SQL for your driver.
Step 5: Validate JSON input
Always validate structure before saving user-submitted JSON. Use dot notation with wildcards for arrays.
$request->validate([
'name' => ['required', 'string', 'max:255'],
'options' => ['nullable', 'array'],
'options.color' => ['nullable', 'string', 'max:50'],
'options.sizes' => ['nullable', 'array'],
'options.sizes.*' => ['string', 'in:S,M,L,XL'],
]);How do you set default values for a JSON column?
A nullable column returns null for new rows, which forces ?? [] checks everywhere. Set a default in the migration with $table->json('options')->default('{}') on databases that allow it, or fill it in the model with an attribute accessor that merges your defaults with the stored array:
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function settings(): Attribute
{
return Attribute::make(
get: fn (?string $value) => array_replace_recursive(
['color' => 'gray', 'sizes' => []],
json_decode($value ?? '{}', true) ?? []
),
set: fn (array $value) => json_encode($value),
);
}This keeps the shape predictable while the database still stores only what was saved. Remember that a model accessor like this replaces the cast for that attribute, so do not declare both for the same column.
Is there a size or performance limit?
JSON documents are loaded as a whole. A column that grows to hundreds of kilobytes slows every query that selects it. If you only need a few keys, select them with the arrow syntax, for example Product::select('id', 'options->color as color')->get(), or move large data to a related table.
Common mistakes
- Forgetting the cast. The attribute then comes back as a JSON string.
- Storing JSON inside a string column and double-encoding it with
json_encode()plus the cast. - Using JSON for data you search by often, instead of a normal indexed column.
- Not validating the incoming structure, which lets arbitrary data into your database.
How do you verify it works?
- Run
php artisan migrateand check that theproductstable has a JSON column. - Create a record in
php artisan tinkerand inspect the raw value withDB::table('products')->first(). It should be a JSON string. - Read the model again and confirm
$product->optionsis an array. - Run a
where('options->color', 'green')query and make sure it returns the record.
FAQ
What is the difference between json and jsonb in Laravel?
jsonb() exists for PostgreSQL and stores a binary form that supports indexes. On other databases it behaves like json().
Can I index a JSON key?
Yes, but the method depends on the database. MySQL uses generated columns, and PostgreSQL uses GIN or expression indexes. Check your database documentation.
Should I use array or collection cast?
Use array for plain data and collection when you want Collection methods on it. Both store JSON the same way.
Is encryption available for JSON?
Yes. Laravel offers the encrypted:array cast. Note that you can no longer query inside an encrypted value.



