PostgreSQL JSONB in Laravel: Indexing &amp; Casting | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Chaos        On this page       1. [  Why JSONB Deserves More Respect (and More Caution) ](#why-jsonb-deserves-more-respect-and-more-caution)
2. [  Indexing JSONB Correctly ](#indexing-jsonb-correctly)
3. [  Querying JSONB in Eloquent ](#querying-jsonb-in-eloquent)
4. [  Avoid This Anti-Pattern ](#avoid-this-anti-pattern)
5. [  Type-Safe Eloquent Casts for JSONB ](#type-safe-eloquent-casts-for-jsonb)
6. [  Key Takeaways ](#key-takeaways)

  ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Chaos](https://cdn.msaied.com/230/16896c4f37a3a231b16de1a2ddb0040d.png)

  #laravel   #postgresql   #eloquent   #jsonb  

 PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Chaos 
================================================================================

     18 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Why JSONB Deserves More Respect (and More Caution)  ](#why-jsonb-deserves-more-respect-and-more-caution)
2. [  02   Indexing JSONB Correctly  ](#indexing-jsonb-correctly)
3. [  03   Querying JSONB in Eloquent  ](#querying-jsonb-in-eloquent)
4. [  04   Avoid This Anti-Pattern  ](#avoid-this-anti-pattern)
5. [  05   Type-Safe Eloquent Casts for JSONB  ](#type-safe-eloquent-casts-for-jsonb)
6. [  06   Key Takeaways  ](#key-takeaways)

 Why JSONB Deserves More Respect (and More Caution)
--------------------------------------------------

PostgreSQL's `jsonb` type is not a dumping ground for schema-averse data. Used deliberately, it solves real problems: sparse attributes, user-defined metadata, and semi-structured payloads that would otherwise demand a dozen nullable columns. Used carelessly, it produces full-table scans and unmaintainable query logic.

This article covers the three areas where Laravel developers most often go wrong: **indexing strategy**, **query construction**, and **Eloquent casting**.

---

Indexing JSONB Correctly
------------------------

The default GIN index covers containment (`@>`) and existence (`?`) operators across the entire document. Create one in a migration:

```php
// database/migrations/2024_01_01_000000_add_gin_index_to_products.php
public function up(): void
{
    Schema::table('products', function (Blueprint $table) {
        $table->jsonb('attributes')->nullable();
    });

    DB::statement(
        'CREATE INDEX products_attributes_gin ON products USING GIN (attributes)'
    );
}

```

If you query a **specific key path** repeatedly, a functional B-tree index is cheaper and more selective:

```sql
CREATE INDEX products_attributes_brand
    ON products ((attributes->>'brand'));

```

In a migration:

```php
DB::statement(
    "CREATE INDEX products_attributes_brand ON products ((attributes->>'brand'))"
);

```

Rule of thumb: GIN for containment queries over unknown keys; functional B-tree for known, high-cardinality key paths.

---

Querying JSONB in Eloquent
--------------------------

Laravel's `whereJsonContains` maps directly to the `@>` operator and benefits from a GIN index:

```php
// Find products tagged with 'waterproof'
Product::whereJsonContains('attributes->tags', 'waterproof')->get();

// Containment on a nested object
Product::whereJsonContains('attributes->dimensions', ['unit' => 'cm'])->get();

```

For scalar key lookups, use `whereJsonPath` (Laravel 10+) or a raw expression:

```php
// whereJsonPath uses jsonpath syntax
Product::whereJsonPath('attributes', '$.brand ? (@ == "Acme")')->get();

// Raw alternative — hits the functional index defined above
Product::whereRaw("attributes->>'brand' = ?", ['Acme'])->get();

```

### Avoid This Anti-Pattern

```php
// Loads every row into PHP — no index used
Product::all()->filter(
    fn($p) => ($p->attributes['brand'] ?? null) === 'Acme'
);

```

Always push JSON filtering to the database layer.

---

Type-Safe Eloquent Casts for JSONB
----------------------------------

Storing raw arrays in a model is a maintenance hazard. A custom cast backed by a value object gives you autocomplete, validation, and a single place to evolve the schema.

```php
// app/Casts/ProductAttributesCast.php
namespace App\Casts;

use App\ValueObjects\ProductAttributes;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class ProductAttributesCast implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): ProductAttributes
    {
        return ProductAttributes::fromArray(
            is_string($value) ? json_decode($value, true) : ($value ?? [])
        );
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): string
    {
        $data = $value instanceof ProductAttributes ? $value->toArray() : $value;
        return json_encode($data, JSON_THROW_ON_ERROR);
    }
}

```

```php
// app/ValueObjects/ProductAttributes.php
namespace App\ValueObjects;

readonly class ProductAttributes
{
    public function __construct(
        public string $brand,
        public array $tags = [],
        public ?array $dimensions = null,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            brand: $data['brand'] ?? '',
            tags: $data['tags'] ?? [],
            dimensions: $data['dimensions'] ?? null,
        );
    }

    public function toArray(): array
    {
        return array_filter([
            'brand' => $this->brand,
            'tags' => $this->tags,
            'dimensions' => $this->dimensions,
        ], fn($v) => $v !== null);
    }
}

```

Register the cast on the model:

```php
protected function casts(): array
{
    return [
        'attributes' => ProductAttributesCast::class,
    ];
}

```

Now `$product->attributes->brand` is a typed string, not a fragile array key.

---

Key Takeaways
-------------

- Use a **GIN index** for containment queries; use a **functional B-tree index** for repeated single-key lookups.
- `whereJsonContains` and `whereJsonPath` push filtering to PostgreSQL — never filter JSONB in PHP collections.
- Wrap JSONB columns in a **custom cast + value object** to enforce structure and gain IDE support.
- `JSON_THROW_ON_ERROR` in your cast's `set` method surfaces encoding bugs immediately rather than silently storing `null`.
- Treat JSONB as a deliberate schema decision, not an escape hatch from migrations.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-chaos-1&text=PostgreSQL+JSONB+in+Laravel%3A+Indexing%2C+Querying%2C+and+Casting+Without+the+Chaos) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fpostgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-chaos-1) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  When should I use a GIN index versus a functional B-tree index on a JSONB column?        Use a GIN index when you query the JSONB document with containment (`@&gt;`) or existence (`?`) operators across arbitrary keys. Use a functional B-tree index when you repeatedly filter or sort on a single, known key path — it is smaller, faster to update, and more selective for high-cardinality values. 

      Q02  Does Laravel's whereJsonContains actually use a PostgreSQL GIN index?        Yes. `whereJsonContains` compiles to the `@&gt;` containment operator in PostgreSQL, which is covered by a GIN index created with `USING GIN (column)`. Verify with `EXPLAIN ANALYZE` to confirm an `Index Scan` rather than a `Seq Scan`. 

      Q03  Can I use a custom JSONB cast alongside Eloquent's built-in array cast?        You can, but the built-in `array` cast returns a plain PHP array with no type guarantees. A custom cast backed by a readonly value object gives you named properties, IDE autocomplete, and a single place to handle schema evolution — worth the extra file for any JSONB column you query or display frequently. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection](https://cdn.msaied.com/239/f588e7cbf8e6d3317a581ce0fa27140d.png) laravel postgresql eloquent 

### PostgreSQL Window Functions in Laravel: Ranking, Running Totals, and Gap Detection

Window functions let you compute rankings, running totals, and gaps directly in SQL without pulling rows into...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 19 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/postgresql-window-functions-in-laravel-ranking-running-totals-and-gap-detection) [ ![Custom Eloquent Casts: Encapsulating Domain Logic Inside Model Attributes](https://cdn.msaied.com/238/8e843e57a34f81f853eedefae629c09b.png) laravel eloquent domain-driven-design 

### Custom Eloquent Casts: Encapsulating Domain Logic Inside Model Attributes

Custom Eloquent casts let you push value-object logic directly into model attributes, keeping controllers and...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 19 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/custom-eloquent-casts-encapsulating-domain-logic-inside-model-attributes) [ !['The Story of PHP' Documentary Teaser Is Out — Watch It Now](https://cdn.msaied.com/237/78daf2c90e319b7a740ec4d48d5280c6.png) PHP Laravel Documentary 

### 'The Story of PHP' Documentary Teaser Is Out — Watch It Now

CultRepo has released a teaser for 'The Story of PHP', an upcoming documentary sponsored by JetBrains. It feat...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 18 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/the-story-of-php-documentary-teaser-is-out-watch-it-now) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
