hnassr/nova-key-value
A Laravel Nova field for editing key-value pairs directly in the admin panel. Store flexible JSON or array data with a clean, repeatable interface, making it easy to manage settings, metadata, and custom options without building a custom form.
The package is an archived Nova 2-era field that renders an editable key/value grid and persists the result as JSON on a single model attribute. Treat it as a vendored dependency rather than a maintained one: read composer.json and src/KeyValue.php before installing, because the version constraints will not resolve against Nova 4/5.
1. Confirm compatibility before you type composer require
# in the package repo (or vendor dir after install)
cat composer.json # expect laravel/nova ^2.0 and php ^7.1
cat package.json # vue ^2.5 — a Nova 4 (Vue 3) app cannot use this
If you're on Nova 4+, stop here and either fork the repo (the Vue component is ~100 lines) or use spatie/laravel-data + a plain Text field. The rest of this assessment assumes Nova 2/3 and PHP 7.x.
2. Install and register
composer require hnassr/nova-key-value
Nova 2 auto-discovers service providers; if it doesn't, add Hnassr\NovaKeyValue\FieldServiceProvider::class to config/app.php. No publish step is required — the compiled dist/js/field.js ships in the repo.
3. Add the column and the cast (both are mandatory)
// migration
$table->json('settings')->nullable();
// app/Models/Product.php
protected $casts = [
'settings' => 'array', // 'json' also works; 'array' is safer for round-trips
];
Without the cast, Nova will hand the component a raw string and the write path will either double-encode the JSON or throw Array to string conversion.
4. Attach the field
use Hnassr\NovaKeyValue\KeyValue;
public function fields(Request $request)
{
return [
KeyValue::make('Settings')
->rules('array')
->hideFromIndex(),
];
}
First use case: a single settings JSON column on a resource you edit rarely — payment gateway options, per-tenant feature flags, product shipping attributes. It is a form editor for a JSON blob, not a relational structure; if you need querying, sorting, or per-key validation, model the keys as real columns instead.
Where to look first: src/KeyValue.php for the fluent API actually exposed (labels, placeholders, default value), resources/js/components/FormField.vue for how the array is serialized on save, and dist/js/field.js to confirm the compiled asset matches the source (it often doesn't in abandoned packages).
Basic resource field with sensible display split
KeyValue::make('Settings')
->rules('array')
->hideFromIndex() // raw JSON in a table cell is unreadable
->displayUsing(function ($value) { // detail view
return collect($value ?? [])
->map(fn ($v, $k) => "{$k}: {$v}")
->implode(' · ');
}),
Seeding defaults for new records
KeyValue::make('Settings')
->default([
'currency' => 'USD',
'timezone' => 'UTC',
])
->resolveUsing(fn ($value) => $value ?? ['currency' => 'USD'])
default() only applies on the create form; if you also want the model to be safe when created outside Nova, back it with a column default or a model booted() hook.
Per-key validation when the field is the source of truth
The field does not validate individual keys — ->rules('array') is the ceiling. Enforce shape in a FormRequest or a custom rule:
->rules(['array:currency,timezone', 'currency.string', 'timezone.timezone'])
// or, more flexibly:
->rules(['array', new ValidSettingsShape()])
array:key1,key2 also doubles as an allow-list, which prevents users from inventing junk keys the rest of your app doesn't understand.
Reading the value in application code
Because everything is a string, normalize at the boundary rather than scattering casts:
class Settings
{
public function __construct(private array $raw) {}
public function bool(string $key): bool
{
return filter_var($this->raw[$key] ?? false, FILTER_VALIDATE_BOOLEAN);
}
}
Bind this as a cast or an accessor (getSettingsAttribute) so controllers and Blade never touch the raw array.
Store on a different attribute than the label
If the human label differs from the column, use ->resolveUsing() / ->fillUsing() pairs rather than renaming the column:
KeyValue::make('Payment Options', 'payment_options')
Single-resource "settings page" pattern
For app-wide config, create a Setting model with one row and a Settings Nova resource whose fields() return a handful of KeyValue fields grouped by concern (billing, mail, integrations). Guard the resource with a policy so only admins see it, and override authorizable() / hide the "create" button via canSee().
The package is abandoned — plan for ownership. Last release June 2019, targeting Nova 2. There is no Nova 4 compatibility, no Vue 3 build, and no security response. If it's load-bearing, copy resources/js/components/FormField.vue into your own nova-components/ package on day one rather than discovering the problem during a Nova upgrade.
Compiled assets may not match the source. Abandoned packages frequently ship a dist/js/field.js older than the .vue file. If a documented option has no effect, cd vendor/hnassr/nova-key-value && npm install && npm run prod inside the package, or fork and build.
Keys are reordered when they look numeric. JSON objects round-trip through JavaScript objects, and integer-like keys ("1", "2") are enumerated first. Use string keys ("opt_1") and never rely on insertion order for display.
Duplicate keys silently collapse. The UI will happily let a user add the same key twice; the JSON encoder keeps the last one. Add a uniqueness check in your FormRequest — the component does not do it.
Everything is a string. "false", "0", "null" and "" all survive as strings. Cast explicitly where consumed, and decide early whether empty values mean "unset" or "empty string" — the field's own empty-state handling differs between create and update.
Empty vs. null. Saving a form with an empty grid typically persists [], not null. If downstream code branches on is_null($model->settings), either make the column nullable() with a ->default(null) or normalize in a mutator.
Nested keys are not supported. meta->theme->color won't work as a field name; the attribute is a flat map. Model hierarchies as separate columns or a dedicated JSON resource.
Validation is shallow by design. ->rules('array') accepts any array. Anything that reads a key in production needs a shape rule, or a missing key becomes a null-reference bug rather than a form error.
Sensitive values leak into Nova payloads. Gateway keys and tokens stored here appear in the index, detail, and the raw JSON response. Combination that works: ->hideFromIndex(), ->hideFromDetail(), an encrypted:array cast on the model — but note the field then shows ciphertext in the form, so you'll need a custom resolveUsing that decrypts for editing and re-encrypts on fill. For true secrets, use laravel/nova's password field or a separate vault, not this.
Debugging checklist, in order: (1) is the column actually JSON and cast? (2) does the browser network response show an object or a string? (3) does dist/js/field.js exist and load — check the Network tab for a 404 on nova-api/scripts? (4) php artisan nova:publish isn't needed here, but php artisan view:clear and a hard browser refresh resolve most stale-asset ghosts.
Extension points. fillUsing() and resolveUsing() let you decouple the field name from the storage attribute, merge defaults, or write to a related model. Subclassing KeyValue is the cleanest way to bake in your rules and display format, and lets you swap out the underlying component in one place if you later fork.
How can I help you explore Laravel packages today?