Storing an array or arbitrary structured data in a single database column — tags, settings, preferences — is handled cleanly in Laravel through Eloquent's array or json attribute casting, transparently handling the encode/decode step.
The migration: a JSON column
Schema::table('products', function (Blueprint $table) {
$table->json('tags')->nullable();
});
Most modern databases (MySQL 5.7+, PostgreSQL) have a native JSON column type — this stores the data as genuine structured JSON rather than a plain text string, and on databases that support it, also allows querying into the JSON structure directly with SQL.
Casting the attribute to an array
class Product extends Model
{
protected $casts = [
'tags' => 'array',
];
}
Working with it as a plain PHP array
$product->tags = ['sale', 'featured', 'new'];
$product->save();
// Later, reading it back:
echo $product->tags[0]; // "sale" — already a real array, no decoding needed
The array cast handles JSON encoding when saving and decoding when reading, entirely transparently — accessing $product->tags returns a genuine PHP array directly, with no manual json_decode() call needed anywhere in application code that uses the model.
Modifying an array attribute correctly
// WRONG — this doesn't persist, since Eloquent doesn't detect the array was mutated in place
$product->tags[] = 'new-tag';
$product->save();
// CORRECT — reassign the whole array
$tags = $product->tags;
$tags[] = 'new-tag';
$product->tags = $tags;
$product->save();
Eloquent's change detection compares the attribute's value before and after — appending directly to the array in place can fail to register as a genuine change in some Laravel versions/configurations, which is exactly why reassigning the full array (as shown in the correct example) is the safer, more reliable pattern.
Using AsArrayObject for a more ergonomic mutation API
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
protected $casts = [
'tags' => AsArrayObject::class,
];
$product->tags[] = 'new-tag'; // this DOES work correctly with AsArrayObject
$product->save();
AsArrayObject wraps the value in an object implementing ArrayAccess that properly tracks in-place mutations — this specifically fixes the direct-append problem above, at the cost of the value no longer being a plain array (though it still behaves like one for most array-style access).
Using the object cast, for accessing nested data more fluently
use Illuminate\Database\Eloquent\Casts\AsCollection;
protected $casts = [
'tags' => AsCollection::class,
];
$product->tags->contains('sale'); // Collection methods work directly
AsCollection casts the stored JSON into a genuine Laravel Collection instance rather than a plain array — worth using specifically when the stored data benefits from Collection's own methods (contains(), map(), filter()) rather than plain array functions.
Querying inside a JSON column
$products = Product::whereJsonContains('tags', 'sale')->get();
whereJsonContains() queries directly into the JSON column's structure at the database level — on a supporting database, this can use a genuine JSON-aware index for reasonable performance, though a heavily and frequently queried JSON field is often still a signal that the data would be better modeled as a proper separate related table instead.