Reader Stacks

Querying JSON Columns in Laravel

MySQL and PostgreSQL both support indexing and querying inside a JSON column directly — Eloquent exposes that through arrow syntax and dedicated whereJson* methods, without raw SQL.

A column cast to json or array in Eloquent doesn't need to be pulled out and filtered in PHP to query by its contents — both MySQL and PostgreSQL support indexing and filtering directly inside a JSON column at the database level, and Eloquent's query builder exposes that through a consistent set of methods.

1. Setting up a JSON column

Schema::table('products', function (Blueprint $table) {
    $table->json('attributes')->nullable();
});
// Model
protected function casts(): array
{
    return ['attributes' => 'array'];
}

2. Querying a specific key with arrow syntax

Product::where('attributes->color', 'red')->get();
Product::where('attributes->dimensions->weight_kg', '>', 2)->get();

The -> syntax inside the column name string is Laravel's shorthand for reaching into a nested JSON key — it translates to the appropriate native JSON path syntax for whichever database driver is configured (MySQL's ->> operator, PostgreSQL's equivalent), so the same query builder code works across both without a driver-specific rewrite.

3. Checking whether an array contains a value

// attributes = {"tags": ["waterproof", "lightweight"]}
Product::whereJsonContains('attributes->tags', 'waterproof')->get();

whereJsonContains() is specifically for checking membership inside a JSON array value — a plain where('attributes->tags', 'waterproof') would look for exact equality against the whole array, not membership within it, and simply wouldn't match.

4. Checking array length

Product::whereJsonLength('attributes->tags', '>', 2)->get();

5. Ordering by a JSON value

Product::orderBy('attributes->dimensions->weight_kg')->get();

6. Indexing a JSON path for performance

Filtering by a JSON key without any index means scanning and parsing the JSON in every row — fine for a small table, genuinely slow at scale. Both MySQL 8+ and PostgreSQL support indexing a specific extracted JSON path directly:

// MySQL: a generated column plus an index on it
ALTER TABLE products ADD COLUMN color VARCHAR(50)
    GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.color'))) STORED;
ALTER TABLE products ADD INDEX idx_color (color);
-- PostgreSQL: a direct expression index, no generated column needed
CREATE INDEX idx_products_color ON products ((attributes->>'color'));

7. When a JSON column is the wrong choice

JSON columns are a reasonable fit for genuinely variable, sparse, or user-defined attributes (custom product fields, flexible settings) that don't justify a full relational schema. For data with a fixed, known structure that's queried and filtered constantly — the kind of thing a proper column or a separate related table handles more efficiently and with cleaner indexing — a JSON column is usually the wrong tool, chosen more for convenience during initial development than for how the data is actually queried afterward.

Topics: Database Queries & Eloquent