PostgreSQL JSONB in Laravel: Indexing &amp; Casting | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Chaos

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

 JSONB columns offer genuine flexibility, but misuse turns them into performance sinkholes. This guide shows how to index, query, and cast JSONB in Laravel without sacrificing type safety or query speed.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 18 Jun 2026 · Updated 18 Jun 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [postgresql](https://www.msaied.com/public/articles?search=postgresql)
- [eloquent](https://www.msaied.com/public/articles?search=eloquent)
- [jsonb](https://www.msaied.com/public/articles?search=jsonb)

 Frequently asked questions 
---------------------------

  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.

   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`.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleHow Laravel News Cached a High-Traffic Site at the Edge with Fast Laravel and Cloudflare](https://www.msaied.com/public/articles/how-laravel-news-cached-a-high-traffic-site-at-the-edge-with-fast-laravel-and-cloudflare) [Next articleLattice: Build Inertia UIs in Pure PHP Without Writing JSX](https://www.msaied.com/public/articles/lattice-build-inertia-uis-in-pure-php-without-writing-jsx)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
