ibrahem-kamal/json-field
A Laravel package for working with JSON database fields—cast, store, and validate structured JSON data through an expressive field API, keeping models clean and queries simple.
1. Install and confirm it actually matches your Nova version.
composer require ibrahem-kamal/json-field
Before writing a single line of resource code, open vendor/ibrahem-kamal/json-field/composer.json and check:
require block — does it pin laravel/nova to ^4.0, ^5.0, or nothing at all?autoload.psr-4 namespace — with 1 star and 0 dependents, the namespace is not something you should guess. It will be something like IbrahemKamal\JsonField\ or IbrahemKamal\NovaJsonField\.dist/js/field.js is committed. If it is not, the field ships source only and you must npm install && npm run prod inside the package directory (or php artisan nova:publish) before the component renders — otherwise you get a blank white card in the form.2. Look here first, in this order:
vendor/ibrahem-kamal/json-field/src/ # the Field class, resolve()/fill() logic
vendor/ibrahem-kamal/json-field/resources/js/ # what the editor actually is (CodeMirror? plain textarea?)
vendor/ibrahem-kamal/json-field/README.md # usually the only docs you get
The two methods that define the entire contract are resolve() (DB → frontend) and fill() (frontend → DB). Read them before trusting anything else, because everything in the Gotchas section below is determined by those two functions.
3. First use case: a meta / settings column.
Add the column if it doesn't exist:
Schema::table('products', function (Blueprint $table) {
$table->json('meta')->nullable();
});
Cast it on the model — this is the step that decides whether you have a good day or a bad one:
protected $casts = [
'meta' => 'array',
];
Then in the Nova resource:
use IbrahemKamal\JsonField\JsonField; // verify against psr-4
public function fields(NovaRequest $request)
{
return [
ID::make()->sortable(),
Text::make('Name'),
JsonField::make('Meta', 'meta')
->rules('nullable', 'array')
->help('Arbitrary key/value metadata, stored as JSON.'),
];
}
Create a record, type {"color":"red","weight":12}, save, then check the raw row with DB::table('products')->first() — not the Nova UI. If the column holds "{\"color\":\"red\"}" (a quoted string), you have the double-encoding bug described below.
Make the editor options explicit. Community JSON fields almost universally expose a meta() bag for editor configuration. Use it rather than editing the compiled JS:
JsonField::make('Payload')
->meta([
'height' => 400,
'readOnly' => false,
'showErrors' => true,
])
->resolveUsing(fn ($value) => $value) // only if you need normalisation
Validate twice: the string and the shape.
JsonField::make('Payload', 'payload')
->rules('required', 'json') // is it parseable at all?
->creationRules('json')
->updateRules('json')
'json' catches malformed input before it hits the DB. If you also want schema validation, do it in a FormRequest or with a custom rule — Nova's rules() runs against whatever the field submits, which may be a JSON string or a PHP array depending on the field's fill() implementation. Assert which one it is with a quick Log::info(gettype($request->payload)) in store().
Format the index/detail views. Raw {"a":1,"b":2} in a table cell is unreadable and blows up row height. Two options:
JsonField::make('Payload')
->hideFromIndex() // simplest
->onlyOnDetail();
// or summarise
JsonField::make('Payload')
->displayUsing(fn ($value) => collect($value)->keys()->take(3)->implode(', '));
Reuse a configured field across resources. Define it once so editor options and validation don't drift:
// app/Nova/Fields/PayloadJson.php
class PayloadJson extends JsonField
{
public function __construct($name = 'Payload', $attribute = 'payload')
{
parent::__construct($name, $attribute);
$this->rules('nullable', 'json')->hideFromIndex();
}
}
Seed and factory data as arrays, not strings. Since the model casts to array, factories should produce ['meta' => ['color' => 'red']]. If you seed with json_encode(...) you'll write a double-encoded string, and the Nova field will then show you a quoted blob on first edit.
Treat the column as opaque in queries. Day-to-day you read the whole document via the model. If you need to filter on a key, either add a generated column in MySQL 5.7+/Postgres (->storedAs("meta->>'color'")) or use whereJsonContains/where('meta->color', 'red') — and then hide the JSON field from index, because it will not be sortable anyway.
Double encoding is the #1 failure mode. The chain is: Eloquent cast array → Nova serialises to JSON → field fill() runs json_encode() again → you store a JSON string instead of a JSON document. Symptoms: the editor shows escaped quotes on second edit, whereJsonContains stops matching, and Postgres jsonb queries return nothing. Verify with:
dd(DB::table('products')->value('meta')); // expect {"color":"red"}, not "{\"color\":\"red\"}"
Fix by overriding fill() on a subclass, or by removing the model cast and letting the field own serialisation entirely. Pick one owner of the encoding and never both.
Empty string vs null on jsonb. If the field submits '' for an untouched editor, PostgreSQL rejects it (invalid input syntax for type json) and MySQL's JSON_VALID fails. Guard in fill()/resolve():
public function fill(NovaRequest $request, $model)
{
$value = $this->valueForIndex($request); // or $request->input($this->attribute)
return parent::fill($request, $model->forceFill([
$this->attribute => blank($value) ? null : $value,
]));
}
->sortable() and ->searchable() silently misbehave on JSON columns. MySQL will order by the document's byte representation; Postgres may error on json (non-jsonb) types. Don't enable them unless you've added a generated column to sort on.
Column size limits bite without warning. MySQL TEXT caps at 64KB and truncates or errors depending on strict mode. If users paste large documents, use MEDIUMTEXT or JSON, and add ->rules('max:60000') as a soft guard.
Nova 4 vs Nova 5 (Vue 2 vs Vue 3). A package with this little activity is unlikely to have been dual-built. If the form renders a blank card, a broken dist/js bundle, or a console error about Vue.component / defineComponent, it's a version mismatch: check package.json for the laravel-nova dev dependency and the Vue major.
If assets look stale, rebuild and re-publish rather than assuming the field is broken:
cd vendor/ibrahem-kamal/json-field && npm install && npm run prod
php artisan nova:publish
php artisan view:clear
Debugging the round-trip. In order: (1) GET /nova-api/{resource}/{id} in the browser network tab — inspect the raw JSON body to see whether the field value is a string or an object; (2) check the PUT/POST payload on save for the same; (3) Log::info() inside resolve() and fill(); (4) storage/logs/laravel.log for validation and DB errors. That sequence localises the fault to DB, Eloquent cast, field class, or Vue component in four steps.
Maintenance risk is the real gotcha. One star, zero dependents, no ecosystem pressure means no security patches and no Nova-5 upgrade path. Treat it as a vendored dependency: fork it into nova-components/json-field inside your own repo (Nova supports local path components), pin the exact version in composer.json, and add a test that asserts resolve()/fill() round-trip a nested array without mutation. If the field is only ever a plain key/value editor, Laravel Nova's built-in KeyValue field or a Textarea with a json rule plus displayUsing(fn ($v) => json_encode($v, JSON_PRETTY_PRINT)) gets you 80% of the value with zero third-party risk — reach for this package only if its syntax-highlighted editor is genuinely worth owning.
How can I help you explore Laravel packages today?