Skip to content

Framework class-based casts handle JSON null differently than string-based casts #61204

Description

@Propaganistas

Laravel Version

13.7.6

PHP Version

8.5.2

Database Driver & Version

MySQL 9.3.0

Description

I've noticed that there's a difference between string-based casts such as array and object vs class-based casts such as AsArrayObject and AsCollection if the target column is of type JSON.

When persisting null, they act differently:

$model->jsonColumn = null;
$model->save();
Cast Database value
array / object null
AsArrayObject / AsCollection "null" (JSON type)

When working with retrieved models all works fine because the JSON type is properly converted back to plain null. But things get skewed when you want to query for the json column being entirely null:

Cast Query MySQL SQLite
array / object whereNull(jsonColumn)
AsArrayObject / AsCollection whereNull(jsonColumn)
AsArrayObject / AsCollection where(jsonColumn, '=', '"null"')
AsArrayObject / AsCollection whereJsonLength(jsonColumn, '<=', 0)
AsArrayObject / AsCollection whereRaw('JSON_TYPE(jsonColumn) = "NULL"')

The main difference is that the string-based casts don't cast if the value is null. They keep it as null in the database as well, resulting in whereNull to be able to query it. The class-based casts do apply their casting, in this case to JSON string "null".

Given that whereNull(jsonColumn->property) IS supported, I would have expected whereNull(jsonColumn) to be supported as well. And also note the difference in results between MySQL en SQLite (haven't checked Postgres or SQL Server yet), denoting a database driver dependency in codebases if the grammars are left as-is.

Steps To Reproduce

See https://github.com/Propaganistas/framework-issue-61204 for a reproducible application.


Schema::create('foos', function (Blueprint $table) {
    $table->id();
    $table->json('items')->nullable();     // but nullable isn't even required here because it will always contain a json type string, even "null"
    $table->timestamps();
});

class Foo extends Model
{
    protected $guarded = [];

    protected function casts(): array
    {
        return [
            'items' => AsCollection::class,
        ];
    }
}

$foo = new Foo;
$foo->items = null;
$foo->save();

Foo::whereNull('items')->exists(); // false

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions