Laravel constraint violation error on JSON field

MariaDB laravel php

I recently encountered a Laravel 'constraint violation' error related to JSON field. The logs were not revealing and the bug was intermittent. It took me some time, but fortunately the fix was easy.

TL;DR

  • Problem: In Laravel's Model class I had set the field to guarded and then provided an invalid default value.
  • Solution: In Laravel make the default values of your model's JSON fields valid JSON encoded strings. Even when they are empty {}.

The Error

When I deployed code to the production environment for the first time, I saw a HTTP-500 error which had not arisen in my local testing:

[2022-09-27 03:54:27] local.ERROR: SQLSTATE[23000]: Integrity constraint violation: 
4025 CONSTRAINT `table.field3` failed for `database`.`table` (SQL: insert into `table` (`field1`, `field2`, `field3`
...
#0 /path/to/project/vendor/laravel/framework/src/Illuminate/Database/Connection.php(719): 
Illuminate\\Database\\Connection->runQueryCallback('insert into `ta...', Array, Object(Closure))

Not a very illuminating (pun intended) log entry, plus I could not reproduce the error in my vagrant development environment. The only difference between the vagrant box and production was that MariaDB was version 10.9 versus version 10.7 in production.

The Problem

It turns out the field type being json() in the Laravel migration was significant.

database/migrations/2022_09_25_133630_create_model_table.php :

new class extends Migration
{
    public function up()
    {
        Schema::create('my_model', function (Blueprint $table) {
            $table->id();
            ...
            $table->json('field3');

I had always wondered why Laravel bothered with so many field types in the migration when they all got reduced to a handful once the SQL tables were created. In this instance the MariaDB column was longtext (non-nullable).

In Laravel's Model class I had set the field to guarded and then provided a default value which I thought was appropriate.

app/Models/my_model.php :

class my_model extends Model
{
    use HasFactory;

    /**
     * The attributes that are NOT mass assignable.
     *
     * @var string[]
     */
    protected $guarded = [
        'field3'
    ];

    /**
     * Default values for attributes.
     *
     * @var array
     */
    protected $attributes = [
        'field3' => "",
    ];

It must be that the underlying mechanism of how the field was stored across the two versions of MariaDB came to the fore here? The error had arisen because an empty string which I had provided as a default value is not a valid JSON encoding and thus not compatible with the Laravel field type of json(). In my defense it is perfectly valid in the context of a non-nullable longtext column in MariaDB.

The Solution

In Laravel make the default values of your model's JSON fields valid JSON encoded strings.

app/Models/my_model.php :

class my_model extends Model
{
    use HasFactory;

    /**
     * The attributes that are NOT mass assignable.
     *
     * @var string[]
     */
    protected $guarded = [
        'field3'
    ];

    /**
     * Default values for attributes.
     *
     * @var array
     */
    protected $attributes = [
        'field3' => "{}",
    ];

Links

Credits

Photo by Raphael Mittendorfer on Unsplash

Previous Post