laragear/json
Laragear/Json makes JSON columns in Laravel easy to work with: cast attributes to typed objects, validate nested data, query values cleanly, and keep Eloquent models tidy. A lightweight toolkit for structured, schema-friendly JSON storage.
This is a major release (v4.0.0), so treat it as a fresh install rather than an in-place bump until you have read the changelog at the link in the release notes.
composer require laragear/json
Laravel's package auto-discovery registers the service provider; there is typically nothing to publish for basic use. Confirm in the README whether v4 adds a publishable config (php artisan vendor:publish --tag=...) — if it does, publish it early so config defaults are explicit in your repo.
Where to look first
Json object).src/Json.php (or equivalent core class) — the public methods are your real API surface: get, set, has, forget, all, toArray, toJson.First use case: a JSON column on an Eloquent model
use Illuminate\Database\Eloquent\Model;
class Account extends Model
{
protected $casts = [
'settings' => \Laragear\Json\Casts\AsJson::class, // confirm exact cast name in README
];
}
$account->settings->get('notifications.email');
$account->settings->set('notifications.email', false);
$account->save();
The point of the package is that settings is no longer a raw array you index into — it is a fluent object with dot-notation reads and writes, so nested updates stop being a mess of Arr::get / data_set.
Second use case: normalising an API payload
$json = json($request->input('payload')); // or the factory shown in the README
$json->get('user.profile.name');
$json->has('user.profile.address');
$json->forget('user.password');
Start with one JSON column or one inbound payload and expand from there; the package does not need to own every array in your application to be useful.
Cast JSON columns, then mutate nested paths
The highest-leverage pattern is the Eloquent cast. Replace array casts on structured columns with the package's JSON cast so that reads and writes are path-aware:
protected $casts = [
'settings' => AsJson::class,
];
// read
$theme = $account->settings->get('ui.theme', 'light');
// write
$account->settings->set('ui.theme', 'dark');
$account->save();
If the cast is immutable, always assign the result back ($account->settings = $account->settings->set(...)) — see the Gotchas section.
Wrap request payloads at the boundary
Convert inbound array/string JSON into the package object in a form request or controller, then pass the object down instead of a loose array. This gives downstream code one consistent accessor style and removes repeated Arr::get calls.
$payload = json($request->validated('payload'));
$payload->has('items.*.sku'); // wildcard checks, if supported in v4
Validation and normalisation pipeline
Use the object after validation, not instead of it. Validate the structure with Laravel rules, then use the JSON object to normalise defaults and strip sensitive keys before persisting:
$data = json($validated);
$data->set('meta.created_at', now()->toIso8601String());
$data->forget('meta.internal_flags');
Build API responses fluently
For endpoints that assemble a response from several sources, build the envelope with the JSON object and let it serialise:
return response()->json(
json(['data' => $resource])->set('meta.version', 'v2')
);
Compose with Eloquent accessors and resources
A clean integration is to keep the cast on the model and expose a typed accessor:
public function notificationEmail(): ?string
{
return $this->settings->get('notifications.email');
}
This keeps dot-notation strings out of your controllers and views.
Use it for configuration-shaped data inside a single column
Feature flags, per-tenant preferences, and webhook payload snapshots are the sweet spot: deep nesting, frequent partial updates, and a need to read a single path without hydrating the whole structure.
Testing pattern
Assert on the serialised form rather than on internal state:
$this->assertSame('dark', $account->fresh()->settings->get('ui.theme'));
$this->assertJsonPath('meta.version', 'v2', $response->json());
v4.0.0 is a major bump — audit before upgrading. The release notes only link to the full changelog, so open https://github.com/Laragear/Json/compare/v3.0.0...v4.0.0 and look specifically for renamed classes, changed cast names, removed methods, and signature changes. Pin v3 in composer.json if you need time to migrate. Run your JSON-column test suite immediately after the upgrade; cast changes fail loudly only when a model is hydrated, so a green suite that never touches those models proves nothing.
Immutability will bite you. If the package exposes an immutable variant (or if v4 makes the default object immutable), $model->settings->set(...) can silently do nothing to the persisted model. Always check whether set returns a new instance and assign it back:
$account->settings = $account->settings->set('ui.theme', 'dark');
$account->save();
A missing save() after a mutating call is the single most common source of "my JSON column isn't updating" bugs.
Dot notation vs literal dots. If any of your JSON keys legitimately contain a ., dot-path access will misread them. Escape or avoid such keys; do not assume the package distinguishes them.
Type coercion at the boundary. JSON round-trips turn integers into floats in some encoders and turn empty objects into arrays. If a downstream consumer expects {} vs [], assert on the serialised output, not on toArray().
Encoding flags. If you need unescaped Unicode or preserved zero fractions, check how the package encodes. json_encode flags are easy to lose when you move data through a wrapper; verify with a payload containing accented characters and 1.0.
Avoid re-wrapping in loops. Building a fresh JSON object per iteration over thousands of rows is measurably slower than mutating one object and serialising once. For bulk exports, collect arrays and cast once at the end.
Debugging. dd($json) can be unreadable because the object proxies array access. Reach for $json->toJson() or $json->toArray() when dumping in a test or Tinker, and assert against the decoded array in tests so failures are diffable.
Extension points. Check the README for macros and custom casts. If you need application-specific accessors, a macro (Json::macro('tenant', ...)) keeps that logic reusable without forking the package.
Watch the cast name. Package majors sometimes rename casts (e.g. an AsJson vs AsJsonObject split). A wrong cast class name in $casts throws at model hydration time, which surfaces far from the line you edited — grep your models for the old class name after upgrading.
Laravel version support. Major package releases often bump the minimum Laravel/PHP version. Check composer.json constraints in the package before upgrading; a failed composer update is usually a PHP constraint, not a bug.
How can I help you explore Laravel packages today?