Open-source project
staudenmeir/eloquent-json-relations avatar
staudenmeir/eloquent-json-relations

eloquent-json-relations: Eloquent Relationships on JSON Columns

Laravel Eloquent relationships with JSON keys

1,080 stars64 forksPHPMIT

At a glance

What is it?
This Laravel extension teaches Eloquent to follow relationships stored inside JSON columns, so a user can belong to a locale through users.options.locale_id and a many-to-many relation can live in a JSON array with no pivot table. Referential integrity still works, through generated columns carrying a real foreign key, and the package is tested against five database engines.
Who is it for?
eloquent-json-relations fits a Laravel application whose data model is genuinely document shaped, where putting a list of related identifiers in a JSON column is the right model and a normalised join table would be noise. It is a poor fit if your schema is relational and the JSON column is only a convenience, since the package adds a trait, a JSON path convention and a query plan that differs from the indexed join you would otherwise have.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 127 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 5, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • staudenmeir/eloquent-json-relations1,080 ★ · +2
Star history
Repository2026-10-212026-11-05
staudenmeir/eloquent-json-relations1.1k1.1k

2 Stars in the last 15 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

A Foreign Key That Lives Inside a JSON Document

This Laravel Eloquent extension adds support for JSON foreign keys to BelongsTo, HasOne, HasMany, HasOneThrough, HasManyThrough, MorphTo, MorphOne and MorphMany relationships, and it also provides many-to-many and has-many-through relationships with JSON arrays.

The mechanism is the JSON path syntax that Laravel already uses for column access. Here is the canonical example from the documentation, where a User has a BelongsTo relationship with a Locale, and there is no dedicated column, because the foreign key locale_id is stored as a property in a JSON field on users.options:

php
class User extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    protected $casts = [
        'options' => 'json',
    ];

    public function locale()
    {
        return $this->belongsTo(Locale::class, 'options->locale_id');
    }
}

The key string in the belongsTo call is the interesting part. It is a JSON path expression, not a column name, and the package translates it into the query the database needs to run. Eloquent's own API is unchanged, so calling the relationship, eager loading it, or constraining the query works the way it always did.

The Trait Goes on Both Sides of the Relationship

The one rule that catches people is in the documentation as a reminder rather than as an error message: remember to use the HasJsonRelationships trait in both the parent and the related model.

The reason is that the package needs to be able to walk a relationship in either direction. When you call $user->locale, the user side has to know how to turn a JSON path into a query, and when you call $locale->users, the locale side has to know how to turn a JSON path into a reverse lookup, which is a different query shape.

php
class Locale extends Model
{
    use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;

    public function users()
    {
        return $this->hasMany(User::class, 'options->locale_id');
    }
}

The trait also brings the two new relationship types into scope, since they live in the package's own namespace. A relationship method that returns one of them has to name the return type explicitly, as in the return type of the relationships in the documentation, so the container knows which implementation to build.

Everything else in the package, from the many-to-many relations to the composite key support, follows the same pattern: an explicit trait, an explicit return type, and a JSON path in the key argument.

Referential Integrity Through Generated Columns

The obvious objection to a foreign key inside a JSON document is that the database can no longer enforce that it points at a real row. The package answers that with generated columns, and it is the part of the design that makes the approach safe to use in production.

On MySQL, MariaDB and SQL Server you can still ensure referential integrity with foreign keys on generated or computed columns. The Laravel migration for MySQL and MariaDB creates a JSON column and then a stored generated column that extracts the value:

php
Schema::create('users', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->json('options');
    $locale_id = DB::connection()->getQueryGrammar()->wrap('options->locale_id');
    $table->unsignedBigInteger('locale_id')->storedAs($locale_id);
    $table->foreign('locale_id')->references('id')->on('locales');
});

The query grammar's wrap method is what produces the correct expression for the platform, because the syntax for extracting a value from JSON differs between engines. The SQL Server version has to cast the extracted value to an integer and use a persisted computed column, since the column type has to be declared.

The result is a real foreign key constraint, enforced by the database, over data that lives in a JSON document. That is what turns this from a convenience into something you can put in front of a schema migration review.

Many-To-Many Without a Pivot Table

The package introduces two new relationship types, BelongsToJson and HasManyJson, for many-to-many relationships with JSON arrays, and in the example there is no pivot table, because the foreign keys are stored as an array in a JSON field on users.options.

By default the relationship stores pivot records as an array of IDs, and the relationship methods are the familiar ones with a different name:

php
public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
{
    return $this->belongsToJson(Role::class, 'options->role_ids');
}

The pivot API is unchanged, which is the point. You can use attach(), detach(), sync() and toggle() on the BelongsToJson side, and the results are what you would expect from a pivot table:

php
$user = new User;
$user->roles()->attach([1, 2])->save(); // Now: [1, 2]

$user->roles()->detach([2])->save();    // Now: [1]

$user->roles()->sync([1, 3])->save();   // Now: [1, 3]

$user->roles()->toggle([2, 3])->save(); // Now: [1, 2]

The documentation also covers the HasOneJson relationship, composite keys, query performance, has-many-through relationships and deep relationship concatenation, so the feature set is broader than the two headline relation types.

Arrays of Objects Instead of Arrays of IDs

The many-to-many relations have a second mode, and it is the one that shows what the package is really for.

Instead of storing an array of IDs, you can store pivot records as objects with additional attributes, and the JSON path changes to point at a property inside each record:

php
public function roles(): \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson
{
    return $this->belongsToJson(Role::class, 'options->roles[]->role_id');
}

The path has three parts. options->roles is the path to the JSON array, the [] marks that the path continues into each element of that array, and role_id is the name of the foreign key property inside the record object.

The attachment then carries the extra attributes along with the key:

php
$user->roles()->attach([1 => ['active' => true], 2 => ['active' => false]])->save();

So the pivot record holds both the related identifier and its own attributes, and the whole thing lives in one JSON column on the parent row. That is a data model you cannot express with a pivot table without a second table or a JSON column anyway, and it is the reason the package exists rather than being a novelty.

The Compatibility Matrix and the Laravel Version Table

The package documents two matrices, and both matter before you install.

The database compatibility list is MySQL 5.7 and later, MariaDB 10.2 and later, PostgreSQL 9.3 and later, SQLite 3.38 and later, and SQL Server 2016 and later. The lower bounds are not arbitrary, because the JSON path expressions and the generated column syntax the package relies on are what set them.

The Laravel version table maps each framework major version to the package version that supports it. Laravel 13.x pairs with 1.15, 12.x with 1.14, 11.x with 1.11, 10.x with 1.8, 9.x with 1.7, 8.x with 1.6, 7.x with 1.5, 6.x with 1.4, 5.8 with 1.3, and 5.5 to 5.7 with 1.2.

That table is the install instruction in practice, because the Composer requirement is stated as a range, `composer require "staudenmeir/eloquent-json-relations:^1.1"`, with a separate form given for PowerShell on Windows.

The quality tooling is visible in the badges: a CI workflow, code coverage through codecov, and PHPStan at level 10, which is the strictest level and a reasonable bar for a package that rewrites how Eloquent builds its queries.

The Test Setup Covers Five Database Engines

The repository ships a compose file that is unusual for a package of this size, and it is the clearest statement of what the project considers its own scope.

There are three PHP services, on 8.3, 8.4 and 8.5, each running a custom image with Xdebug loaded through a shared ini file, and each mounted on the project directory so the tests can run against the checked out code. The images come from the maintainer's own registry rather than from the official PHP images, which is the pattern for a maintainer who wants a consistent development environment across PHP versions.

And then there are four databases, MySQL, MariaDB, PostgreSQL and SQL Server, each with its own credentials and, for MySQL and SQL Server, a health check. The SQL Server health check runs sqlcmd with a SELECT 1 query through the Microsoft tools, and MySQL uses mysqladmin ping.

That is four database engines, and it is the reason the compatibility matrix in the README is credible. A package whose whole purpose is to emit different SQL for different engines is not much use if it is only tested against one of them, and the compose file is how the project avoids that.

The rest of the repository follows the usual PHP layout, with src/ for the implementation, tests/ for the suite, types/ for the static analysis stubs, a phpunit.xml.dist, phpstan.neon.dist and phpstan.types.neon.dist for the analyser configuration, a separate docker-compose.ci.yml for continuous integration, and a .docker/ directory holding the development images.

Editorial conclusion

eloquent-json-relations fits a Laravel application whose data model is genuinely document shaped, where putting a list of related identifiers in a JSON column is the right model and a normalised join table would be noise. It is a poor fit if your schema is relational and the JSON column is only a convenience, since the package adds a trait, a JSON path convention and a query plan that differs from the indexed join you would otherwise have. Before adopting it, check the version table, because the package version is tied to the Laravel major version and the current 1.15 line is for Laravel 13, and read the query performance section, since the package makes a point of documenting how the JSON paths are executed. The package is MIT licensed, the newest release is v1.15 from 2026-03-01, and the last push was on 2026-07-04.

Frequently asked questions

What does eloquent-json-relations add to Laravel Eloquent?

It adds support for JSON foreign keys to BelongsTo, HasOne, HasMany, HasOneThrough, HasManyThrough, MorphTo, MorphOne and MorphMany relationships, plus two new relationship types, BelongsToJson and HasManyJson, for many-to-many relationships stored in JSON arrays with no pivot table.

How do I install eloquent-json-relations?

With `composer require "staudenmeir/eloquent-json-relations:^1.1"`, or with a different form of the version constraint in PowerShell on Windows. The package version is tied to the Laravel major version, with 1.15 for Laravel 13 and 1.14 for Laravel 12.

Can I keep referential integrity with JSON foreign keys?

Yes. On MySQL, MariaDB and SQL Server you can still ensure referential integrity with foreign keys on generated or computed columns, and the README shows the Laravel migrations that create a stored or persisted computed column extracting the value from the JSON document and reference it with a foreign key.

How do I store pivot attributes with a JSON many-to-many relation?

Store the pivot records as objects with additional attributes, and point the relation at the property inside each record with a path such as options->roles[]->role_id. Attaching then takes an array of key and attribute pairs, for example attach([1 => ['active' => true], 2 => ['active' => false]]).

Which databases and Laravel versions are supported?

MySQL 5.7 and later, MariaDB 10.2 and later, PostgreSQL 9.3 and later, SQLite 3.38 and later, and SQL Server 2016 and later, and the repository ships a compose file that runs the test suite against MySQL, MariaDB, PostgreSQL and SQL Server on PHP 8.3, 8.4 and 8.5.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. staudenmeir/eloquent-json-relations on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/staudenmeir-eloquent-json-relations.svg)](https://hysenlabs.com/projects/staudenmeir-eloquent-json-relations)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/staudenmeir-eloquent-json-relations.svg)](https://hysenlabs.com/projects/staudenmeir-eloquent-json-relations)

Was this useful?