# Laravel: Casting Eloquent JSON Fields

I have a short post today to cover something I recently used in a project. I had a table using a JSON column and though they are extremely flexible, I like to know what data exists in the column. To accomplish this, I use a simple data transfer object.

In my project, I have a `checkout_events` table where I track various options and properties of a user's checkout. It’s primarily for debugging purposes, but maybe one day, it could grow into an event sourcing flow.

In the table, I have a `metadata` column and I want to track things like `processor`, `checkoutSource`, `checkoutType`, `errorMessage`, and `exceptionClass`. As things change or grow, I knew the different properties could change, which is why I opted for a JSON column versus a dedicated column for each property.

To make sure I have the correct data going in and out of the table, I created the DTO below:

```php
<?php

readonly class CheckoutMetadata
{
    public function __construct(
        public Processor $processor,
        public CheckoutSource $checkoutSource,
        public CheckoutType $checkoutType,
        public ?string $errorMessage = null,
        public ?string $exceptionClass = null,

    ) {}
}
```

Notice, the `$errorMessage` and `$exception` properties are nullable since the event won’t always have that metadata.

Now, for the model, we need to add a cast for this:

```php
<?php

class CheckoutEvents extends Model
{
    protected function casts(): array
    {
        return [
            'metadata' => CheckoutMetadata::class,
        ];
    }
}
```

Now, to actually make this work, we could create a `Cast` in Laravel as shown in the [docs](https://laravel.com/docs/11.x/eloquent-mutators#custom-casts). However, I am going to opt for making the DTO itself be `Castable` ([docs](https://laravel.com/docs/11.x/eloquent-mutators#castables)) and use anonymous cast classes ([docs](https://laravel.com/docs/11.x/eloquent-mutators#anonymous-cast-classes)). To achieve this, `CheckoutMetadata` needs to implement `Illuminate\Contracts\Database\Eloquent\Castable`. The contract requires a `castUsing` method that returns a new class instance that implements `Illuminate\Contracts\Database\Eloquent\CastsAttributes`:

```php
<?php

class CheckoutMetadata implements Castable
{
    public function __construct(
        public Processor $processor,
        public CheckoutSource $checkoutSource,
        public CheckoutType $checkoutType,
        public ?string $errorMessage = null,
        public ?string $exceptionClass = null,
    ) {}

    public static function castUsing(array $arguments): CastsAttributes
    {
        return new class implements CastsAttributes
        {
			// Get method is called when getting metadata from the model.
            public function get(Model $model, string $key, mixed $value, array $attributes)
            {
				// Convert the JSON data from the database into an object.
                $data = json_decode($attributes[$key]);

                return new CheckoutMetadata(
                    processor: Processor::from($data->processor),
                    checkoutSource: CheckoutSource::from($data->checkout_source),
                    checkoutType: CheckoutType::from($data->checkout_type),
                    errorMessage: data_get($data, 'error_message'),
                    exceptionClass: data_get($data, 'error_message'),
                );
            }

			// Set method is called when updating the metadata field on the model.
            public function set(Model $model, string $key, mixed $value, array $attributes): array
            {
				// Throw an exception if trying to set a value that is not an instance of CheckoutMetadata.
                if (! $value instanceof CheckoutMetadata) {
                    throw new InvalidArgumentException('A CheckoutMetadata instance is required.');
                }

                $data = [
                    'processor' => $value->processor->value,
                    'checkout_source' => $value->checkoutSource->value,
                    'checkout_type' => $value->checkoutType->value,
                    'error_message' => $value->errorMessage,
                    'exception_class' => $value->exceptionClass,
                ];

				// JSON encode the data to store in the database.
                return [$key => json_encode($data)];
            }
        };
    }
}
```

Quite a lot of new stuff here. The `get` method of the anonymous class gets called when we try to get the `metadata` property from the model, like `$checkoutModel->metadata`. We need to decode the JSON from the database and then instantiate a new `CheckoutMetadata` class instead. Now, anytime we call `$checkoutModel->metadata`, we get the `CheckoutMetadata` class and know what properties we have available.

The `set` method is used when we try to update the `metadata` property, so: `$checkoutModel->metadata = $checkoutMetadata`. Since we always want to require data to be in the form of `CheckoutMetadata`, we throw an `InvalidArgumentException` if the value passed in is not an instance of `CheckoutMetadata`.

Now, our model can be used like the following:

```php
<?php

$metadata = new CheckoutMetadata(
  checkoutSource: CheckoutSource::Brick,
  checkoutType: CheckoutType::OneTime,
  processor: Processor::Braintree
);

$model = new CheckoutEvent();
$model->name = 'New Event';
$model->user_id = 1;
$model->metadata = $metadata;
$model->save();

$model->metadata;

// App\CheckoutMetadata {#2744
//    +processor: App\Processor {#2746
//      +name: "Braintree",
//      +value: 2,
//    },
//    +checkoutSource: App\CheckoutSource {#2750
//      +name: "Brick",
//      +value: 2,
//    },
//    +checkoutType: App\CheckoutType {#2748
//      +name: "OneTime",
//      +value: 2,
//    },
//    +errorMessage: null,
//    +exceptionClass: null,
//  }
```

In the future, if we wish to add properties that we will track, we can add new optional properties to the DTO and update the anonymous `CastAttributes` class. If we want to remove properties, we can just delete them from the DTO and update the anonymous `CastsAttributes` class.

For further learning, look into the `Arrayable` and `JsonSerializable` interfaces in Laravel so you can return the DTO to an expected structure when converting a model to an array or JSON. You can also read about [Array / JSON Serialization](https://laravel.com/docs/11.x/eloquent-mutators#array-json-serialization) in Laravel.

I hope you enjoyed this post and learned something new about Eloquent attribute casting and JSON fields. They can be powerful, but it’s important to make sure the data being passed in and out of the field is what you expect. You don’t want it to blow up with countless unknown properties and different structures in each row.

Thanks for reading!

Furthermore, if you’d like to learn more about data transfer objects, read my post [Streamlining API Responses in Laravel with DTOs](https://seankegel.com/streamlining-api-responses-in-laravel-with-dtos?source=more_series_bottom_blogs).
