# spatie/laravel-schemaless-attributes puts JSON columns behind Eloquent syntax

> This package gives an Eloquent model a single JSON column that holds arbitrary attributes, accessed with object syntax or an array, queried through a model scope, and installed with a composer command plus one macro and one cast. It is a small, opinionated library, and the interesting question is when a JSON column is the right answer and when it is a schema in disguise.

**spatie/laravel-schemaless-attributes** — Add schemaless attributes to Eloquent models

- Repository: https://github.com/spatie/laravel-schemaless-attributes
- Website: https://spatie.be/en/opensource
- Stars: 1,085 · Forks: 53
- Language: PHP
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-laravel-schemaless-attributes

## A trait and one JSON column, not a second database

The package describes itself as giving you a bit of the spirit of NoSQL available in Eloquent. It provides a trait that, when applied to a model, allows you to store arbitrary values in a single JSON column.

The framing matters, because the obvious alternative is to run a document database alongside the relational one and stitch the two together in the application layer. This package does not do that. The data stays in the same database, in the same row, in a column your existing migrations and backups already cover. The readme is blunt about the fact that the storage is a JSON column and the requirement is explicit: the package requires a database with support for json columns, like MySQL 5.7 or higher.

So the tradeoff is not relational versus document. It is a real column with a fixed name and a flexible payload, versus a real column for every field. That is a different, narrower and much cheaper decision than changing databases, and it is the reason this package is a good fit for a lot of teams that have considered and rejected MongoDB.

The audience is Laravel developers who hit a specific problem: a table where some records need attributes that most records do not, and where adding a nullable column for each one is starting to feel wrong. Options that vary per record, per user or per integration, a settings blob, a third party payload you want to keep but not model.

The package is by Spatie, and it sits in a family of small, single purpose Laravel packages that share a house style, which is visible in the readme and in the file layout of the repository.

## Object syntax over an array, and dot notation for the nested parts

The API is where the package earns its name, because a JSON column in Eloquent is otherwise an array you have to encode yourself.

The primary access is object style. You set a value with an arrow and read it back the same way:

```php
// add and retrieve an attribute
$yourModel->extra_attributes->name = 'value';
$yourModel->extra_attributes->name; // returns 'value'

// you can also use the array approach
$yourModel->extra_attributes['name'] = 'value';
$yourModel->extra_attributes['name'] // returns 'value'
```

You can also set multiple values in one go by assigning an array, which replaces all existing schemaless attributes, or by calling set with an array.

Retrieval supports dot notation, which is what makes the nested case usable. The get method takes a path, and it takes a default:

```php
// retrieving values using dot notation
$yourModel->extra_attributes->get('rey.side'); // returns 'light'

// retrieve default value when attribute is not exists
$yourModel->extra_attributes->get('non_existing', 'default'); // returns 'default'
```

And there is a forget method to delete a key and its value.

What the package adds over a raw cast is therefore a small, predictable surface: write with either syntax, read with a path, read with a fallback, delete a key, and replace everything. The dot notation is the piece that matters, because a nested payload read as an array of mixed types is where the real friction would be.

One caveat is worth stating plainly, because it is a property of the storage rather than of the code. Nothing in a JSON column is typed or validated. The package gives you a convenient way to store and fetch values, and the values themselves carry whatever discipline your application imposes on them.

## modelScope is what makes the data queryable

A schemaless column that cannot be queried is a write only blob, and this package does not stop there.

Every schemaless column exposes a modelScope, and the model defines a scope method that returns it:

```php
public function scopeWithExtraAttributes(): Builder
{
    return $this->extra_attributes->modelScope();
}
```

With that in place, you can retrieve models by their schemaless attributes, and the readme gives the shape of the call:

```php
// it has a modelScope to retrieve all models with the given schemaless attributes
$yourModel->withSchemalessAttributes(['name' => 'value', 'name2' => 'value2'])->get();
```

This is the feature that separates the package from writing your own accessor, and it is also the feature with the most operational consequence, because a query against a JSON column is not the same as a query against an indexed column. MySQL and PostgreSQL can index parts of a JSON document, but that is a deliberate step you take, and it is not something the package does for you.

So there is a design line here worth drawing for yourself before you adopt it. Schemaless attributes suit values you write often and read by key, for example a stored payload, a per user preference set, a small bag of integration metadata. They are a poor fit for values you filter and sort on, because every such filter is a JSON path predicate rather than an index lookup.

The readme does not discuss index strategy or query performance, so this is a judgement to make rather than a documented constraint, and it is the one that decides whether the package helps you or quietly makes your reporting slower.

## Install is a composer command, a macro, a cast and a scope

The installation is short and the setup has three parts.

The package installs through composer:

```bash
composer require spatie/laravel-schemaless-attributes
```

The readme notes that for Laravel versions 6 and 7, or PHP 7, you should use version 1.x of the package, which is the kind of version floor worth checking before you add a dependency to an older application.

Then you need the column. You add a migration for every model that will carry schemaless attributes, and you use the schemalessAttributes method on the Blueprint, passing the column name:

```php
Schema::table('your_models', function (Blueprint $table) {
    $table->schemalessAttributes('extra_attributes');
});
```

The readme is explicit that you can use any name you like, and that you are free to add as many schemaless attribute columns to your table as you want. In every example in the readme the column is called extra_attributes, and the same name is used throughout this article.

Finally you prepare the model, which means a custom cast and a scope:

```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;

class TestModel extends Model
{
    public $casts = [
        'extra_attributes' => SchemalessAttributes::class,
    ];

    public function scopeWithExtraAttributes(): Builder
    {
        return $this->extra_attributes->modelScope();
    }
}
```

That is the whole integration: one migration macro, one cast entry, one scope method. The readme also suggests that if you want to reuse the behaviour across multiple models you can put the function in a trait of your own, and it shows one, a HasSchemalessAttributes trait whose initializer sets the cast.

Three pieces of plumbing per model is a small price, and the fact that the cast is a plain class in the $casts array means it plays by Eloquent's rules rather than fighting them.

## Two columns on one model need the trait, not two casts

There is one structural case the readme handles explicitly, and it is the case where a single model carries more than one schemaless column.

For a model with extra_attributes and other_extra_attributes, you are told to use the SchemalessAttributesTrait. The trait holds a protected array naming the columns, and each column still gets its own scope method:

```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Spatie\SchemalessAttributes\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;

class TestModel extends Model
{
    use SchemalessAttributesTrait;

    protected $schemalessAttributes = [
        'extra_attributes',
        'other_extra_attributes',
    ];

    public function scopeWithExtraAttributes(): Builder
    {
        return $this->extra_attributes->modelScope();
    }

    public function scopeWithOtherExtraAttributes(): Builder
    {
        return $this->other_extra_attributes->modelScope();
    }
}
```

The design point is that the trait does not remove the need for scopes. Each column still gets a named scope that returns its modelScope, so the trait saves the array declaration and the plumbing, not the vocabulary.

That is a small thing, but it is the shape of the library everywhere. The package does not invent a new query language for JSON. It gives you a cast that returns a collection-like object with dot notation, and a scope that turns a key value filter into a query, and everything else is ordinary Eloquent.

For a team that has been tempted to add a MongoDB instance to hold one flexible column, this is the version of that idea that stays inside the database you already operate, with the trade-off that the flexible data is not indexed by default and the queries against it are not the fast kind.

## A column per field, or one column with a payload

The real alternative to this package is not another package, it is the schema you already have, and the comparison is worth making plainly.

The classic Laravel approach is a nullable column per attribute, with a migration for each new field, a fillable entry, and a column that most rows leave empty. The advantages are real. The column is typed, the database validates it, the optimiser can use an index on it, and a query on it is a query on an index. The costs are also real. Every new attribute is a migration that has to be deployed, the model gets wider, and a set of attributes that only two percent of rows use still takes space in every row.

This package moves the balance. You get one column, typed by the database as JSON, that holds whatever you put in it, and you can add attributes without a migration. The costs are that nothing inside the column is validated, that filtering on it is a JSON path predicate rather than an index lookup, and that a value you store today has no shape contract for the application layer.

The other alternative, a document database, moves the data out of the relational database entirely, and that is a much bigger decision than the one this package asks you to make. For a team that has already decided against a second database, this package is the middle path, and it is a genuinely useful one for the case where the data is genuinely irregular, such as a third party payload you want to keep intact.

There is a third option to keep in mind, which is to model the irregular data properly, with a related table and a foreign key, and accept the extra join. That is the answer when the data has any structure worth constraining, and it is the answer that keeps reporting queries fast.

## Maintenance, licence, and the shape of the package

The licence is MIT, in the LICENSE.md file at the root of the repository, and the project is on Packagist as spatie/laravel-schemaless-attributes with a release badge and a download badge in the readme.

On maintenance, the repository is not archived and the last push was on 2026-06-02, a little under five months before this writing. The release history shows 2.5.1 on 2025-02-10, 2.5.2 on 2026-02-09 and 2.6.0 on 2026-02-21, so this is a package that moves in small steps and occasionally makes a minor version jump.

The repository layout is the smallest in this category of package. There is a src directory with the code, a tests directory, a composer.json, a phpunit.xml.dist, and a .php_cs.dist.php for the code style rules, alongside the CHANGELOG.md, the licence and the readme. There is no configuration file, no service provider to publish and no migrations to run, because the migration is the macro you call yourself. That is worth noting as a design choice, since most Laravel packages ship a service provider and a config file even when they do not need one.

The readme also carries the usual Spatie support material, an offer to buy a paid product and a request for a postcard, which is a small reminder that these packages are maintained by a business and that the support channel is a commercial one rather than a community forum.

For an adopter, the practical risk is low and the upgrade cost is low too, because the surface area is one cast, one scope and a handful of methods. The bigger question is the one in the previous section, and that is a decision about your data rather than about the package.

## Conclusion

spatie/laravel-schemaless-attributes is worth adopting when you have genuinely open ended per record data, settings that differ per user, or a prototype where a migration for every new field is in the way, because it gives you that flexibility in one column with query support rather than in a separate document store. It is the wrong choice for data you intend to query, filter and report on, since a JSON column makes those queries slower and unindexed, and it is the wrong choice if you need validation. Before you adopt it, confirm your database supports json columns, and note that the current release is 2.6.0 from 2026-02-21 with the last push on 2026-06-02.

## FAQ

### What does spatie/laravel-schemaless-attributes do?

It provides a trait that, when applied to an Eloquent model, allows you to store arbitrary values in a single JSON column, giving you some of the spirit of NoSQL inside Eloquent. The values are written and read with object or array syntax and dot notation.

### How do I install spatie/laravel-schemaless-attributes?

With composer require spatie/laravel-schemaless-attributes, then add a migration using the schemalessAttributes method on the Blueprint to create the column, and add a cast plus a scope method to the model. The package requires a database with json column support such as MySQL 5.7 or higher, and for Laravel 6 and 7 or PHP 7 you should use version 1.x.

### How do I query Eloquent models by their schemaless attributes?

Each schemaless column exposes a modelScope, and the model defines a scope method that returns it, for example a scopeWithExtraAttributes method returning $this->extra_attributes->modelScope(). You can then retrieve models with withSchemalessAttributes(['name' => 'value'])->get().

### Can a Laravel model have more than one schemaless attributes column?

Yes, and in that case the readme says to use the SchemalessAttributesTrait and list the column names in the protected $schemalessAttributes property. Each column still gets its own scope method, so the trait saves the declaration rather than the vocabulary.

### How do I delete a schemaless attribute in Laravel?

Use the forget method on the attribute collection, for example $yourModel->extra_attributes->forget('key'). The readme also documents a get method with a fallback value, so $yourModel->extra_attributes->get('non_existing', 'default') returns the default.

## Sources

- [License: MIT](https://github.com/spatie/laravel-schemaless-attributes/blob/main/LICENSE)
- [Project website](https://spatie.be/en/opensource)
- [README](https://github.com/spatie/laravel-schemaless-attributes/blob/main/README.md)
- [Releases](https://github.com/spatie/laravel-schemaless-attributes/releases)
- [spatie/laravel-schemaless-attributes on GitHub](https://github.com/spatie/laravel-schemaless-attributes)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-laravel-schemaless-attributes
