invaders-xx/filament-jsoneditor
A Filament form field that adds a full-featured JSON editor to your Laravel admin panels. Edit, validate, and format JSON directly inside Filament forms with syntax highlighting, tree view, and a smooth, developer-friendly editing experience.
Install the package alongside a Filament v3 panel:
composer require invaders-xx/filament-jsoneditor
php artisan filament:assets # or: php artisan vendor:publish --tag=filament-jsoneditor-assets
Check the version line first. The current release is 5.0, whose only shipped change is Laravel 12 support (PR #41). That means 5.x is a framework-constraint release, not a feature release: if you're on Laravel 10/11, you stay on 4.2.1; if you're on Laravel 12 you need 5.x. Nothing in the field's fluent API changed between 4.2.1 and 5.0 as far as the changelog shows — so anything you learn about the component applies to both lines.
The field is a regular Filament form component, so it inherits everything you already know (label(), helperText(), required(), columnSpan(), hidden(), afterStateUpdated(), dehydrateStateUsing(), etc.). The only new thing is the visual editor, which wraps the jsoneditor JS library in an Alpine component bound to the Livewire state.
First real use case — a raw JSON column on a model:
// migration
$table->json('payload')->nullable();
// resource
use InvadersXX\FilamentJsoneditor\Forms\JSONEditor;
JSONEditor::make('payload')
->label('Webhook payload')
->columnSpanFull()
->rules(['nullable', 'json']);
Where to look first:
resources/views/…) — it shows the exact Alpine wiring, the container height, and how the value is passed in/out. That file is also your override point.mode(), modes(), search(), navigationBar(), statusBar(), menuBar(), onChange(), …). Note this API is identical on 4.2.1 and 5.0.State handling (do this one thing right). The editor speaks strings; Eloquent's array/json cast speaks arrays. Pick one contract and stick to it.
// Option A — store a plain JSON string, no cast on the model
JSONEditor::make('payload')
->rules(['nullable', 'json']);
// Option B — keep the `array` cast and convert at the field boundary
JSONEditor::make('payload')
->formatStateUsing(fn ($state) => is_array($state)
? json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
: $state)
->dehydrateStateUsing(fn ($state) => blank($state) ? null : json_decode($state, true))
->rules(['nullable', 'json']);
Option B is the one I'd default to: the rest of the app keeps working with arrays while the admin edits pretty-printed JSON. Because 5.0 changed nothing at the state boundary, this pattern carries over from 4.x unchanged.
Read-only / audit views. ->disabled() alone doesn't reliably stop a JS editor from accepting keystrokes. Use the editor's own read-only mode instead, and drop the bar chrome so it looks like a viewer:
JSONEditor::make('payload')
->mode('view')
->modes(['view'])
->menuBar(false)
->navigationBar(false)
->statusBar(false)
->columnSpanFull();
Tree mode is the differentiator. If your audience is non-developers editing nested config (feature flags, SEO schema, shipping rules), start in tree and allow code as a secondary mode:
JSONEditor::make('settings')
->modes(['tree', 'code', 'form'])
->mode('tree')
->search(false)
->columnSpanFull();
For developer-only raw payloads, skip the UI affordances and go straight to code mode with history enabled.
Settings / custom pages. Inside a custom Filament page with a form, treat it exactly like any other field — the page's form->fill() / getFormState() flow works unchanged. Same for relation managers and modals, provided the editor lives in a single field (not a repeater item — see Gotchas).
Table listing. The package is a form component only. For the index table, don't render the editor; show a summary column:
TextColumn::make('payload')
->formatStateUsing(fn ($state) => is_array($state)
? json_encode($state, JSON_UNESCAPED_SLASHES)
: (string) $state)
->limit(60)
->wrap()
->tooltip(fn ($state) => Str::limit((string) json_encode($state), 500)),
Validation beyond json. The json rule only checks syntax. Layer shape checks with a closure or a dedicated rule so a syntactically valid but semantically wrong document doesn't reach production:
->rules([
'nullable',
'json',
function (string $attribute, $value, Closure $fail) {
$decoded = json_decode($value, true);
if (! is_array($decoded) || ! array_key_exists('url', $decoded)) {
$fail('Payload must contain a "url" key.');
}
},
])
Testing. Livewire/Filament tests don't need a browser — fill the field with a JSON string:
livewire(EditWebhook::class, ['record' => $webhook->id])
->fillForm(['payload' => json_encode(['url' => 'https://example.test'])])
->call('save')
->assertHasNoFormErrors();
Audit trail. Pair it with a afterStateUpdated hook to diff old vs. new JSON into an activity log — config edits are exactly the kind of change reviewers want history on.
composer.json before upgrading, and pin explicitly: "invaders-xx/filament-jsoneditor": "^5.0" on Laravel 12, "^4.2" otherwise. Don't let a floating constraint drag you across the boundary by accident.JsonEditor vs JSONEditor, Forms\… vs Components\…). Run composer show invaders-xx/filament-jsoneditor and read the source tree once, then standardise across your codebase. Do this again after the 5.0 bump — even a constraints-only major can carry incidental cleanups."filament/filament" in lockstep with the plugin.jsoneditor JS/CSS is served from a CDN or published locally. If it's a CDN, air-gapped and corporate-proxy environments will render an empty box with no visible error — a classic "works locally, broken in staging" failure. Publish/host the assets locally if you can, and re-run filament:assets after the upgrade in case the published set changed.columnSpan(1) in a grid — use columnSpanFull() and, if you need more room, override the package's view or add panel CSS targeting the container rather than hacking inline styles.live() on large documents is a request storm. The editor pushes a change event per keystroke burst. Leave the binding deferred (default) for big payloads; reserve ->live(debounce: 800) for small documents where you genuinely need reactive dependent fields.Textarea with ->rows(20) inside repeaters and keep the JSON editor for top-level fields."null". Clearing the editor can leave the string "null" or "" rather than PHP null. Normalise in dehydrateStateUsing() (blank($state) ? null : …) so nullable columns stay genuinely nullable.->jsonEditorOptions([...])-style passthrough (or overriding the Blade view via vendor:publish) is a small, upgrade-friendly patch.App\Forms\JsonEditor subclass (or a Blade component) so a future swap to a CodeMirror-based field is a one-file change, and keep the version pinned in composer.json.How can I help you explore Laravel packages today?