Skip to content

Chapter 4 — Cast it on a model

The question: how do I use a shape as an attribute on an Eloquent model?

This is what JsonShape is for. With one cast, a JSON column is decoded into your shape on read and encoded back to JSON on save — no accessors, no mutators, no manual json_decode. This chapter goes end to end: migration, cast, usage.

It uses the TraceShape from Chapter 1.

1. The migration

Store the data in a plain json column, exactly as you would for any JSON attribute:

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('examples', function (Blueprint $table) {
            $table->id();
            $table->json('trace');
            $table->timestamps();
        });
    }
};

JsonShape doesn't change your schema — it changes how PHP sees the column.

2. The cast

Point an AsJsonShape cast at your shape using AsJsonShape::of(...). On read, the column's JSON is decoded into a TraceShape; on save, the shape is encoded back to JSON.

php
<?php

namespace App\Models;

use App\Shapes\TraceShape;
use Illuminate\Database\Eloquent\Model;
use Plumthedev\JsonShape\Casts\AsJsonShape;

/**
 * @property TraceShape $trace
 */
class Example extends Model
{
    protected $fillable = ['trace'];

    /** @return array<string, mixed> */
    public function casts(): array
    {
        return [
            'trace' => AsJsonShape::of(TraceShape::class),
        ];
    }
}

Best practice

Add the @property TraceShape $trace docblock on the model. The cast handles the runtime conversion; this line tells your IDE and PHPStan the attribute's type, so autocomplete and analysis work on $model->trace.

Need an untyped shape?

Cast with AsJsonShape::class (no ::of(...)) and the column decodes into a plain JsonShape — still array-accessible and serializable, just without a dedicated class. Useful for genuinely loosely-structured columns.

3. Using it

Once cast, the shape is just an attribute. Read it with the accessors from Chapter 2, mutate it with the setters from Chapter 3, then persist with save():

php
$model = Example::find(1);

// read — typed, autocompleted
$model->trace->traceId;
$model->trace->getDuration();

// write — mutate the shape, then persist as usual
$model->trace->setContext([
    'userId'      => 'e92fd2c9',
    'levelNumber' => 16,
]);

$model->save();

You can also assign a brand-new shape to the attribute — it's encoded to JSON on save just the same:

php
$model->trace = TraceShape::make([
    'traceId' => 'abc-123',
    'message' => 'Request handled',
    'context' => ['userId' => 'u-1', 'levelNumber' => 1],
]);

$model->save();

Pitfall

Mutating the shape changes the in-memory object only. Nothing is written to the database until you call $model->save() — the same rule as any other Eloquent attribute.

What about null?

If the column is null (or holds non-array JSON), the attribute reads back as null rather than an empty shape. Assigning null and saving writes null back. Guard with a null check, or seed a default with TraceShape::empty() when you create the row.

Best practices

  • Always pair the cast with a @property docblock so tooling sees the type.
  • Build new shapes with the factory methods rather than raw arrays when you want the value typed before it's assigned.
  • Remember save() — mutating a shape is not persistence.

Previous: Chapter 3 — Write values · Next: Chapter 5 — Create & combine shapes →