A Laravel package that transforms nested JSON/arrays into flat, readable key-value pairs for display in Nova's KeyValue field.
Not everyone understands JSON — especially regular admin users in your Nova Admin Panel. Nova's built-in KeyValue field doesn't work with nested objects or JSON arrays; it simply displays them as raw JSON strings with index numbers as keys, or sometimes doesn’t display them at all.
Without this package:
This package transforms your JSON data into flat, readable key-value pairs before displaying them using Laravel Nova's existing KeyValue fields.
With this package:
- 🔄 Handles JSON strings or arrays
- 🔍 Intelligently handles database relationships, resolving foreign keys by fetching and displaying the related record's name or any column you specify.
- 🎯 Automatically flattens nested objects
- 🎨 Pretty key formatting (snake_case → Title Case)
- 🚫 Smart filtering (exclude by prefix/suffix)
- 📊 Multiple items displayed as separate fields
- ✨ Custom formatters for any field
- 🔒 Read-only, detail-only display
- 🎨 Fluent API for clean, readable code
- 🧰 Built-in formatters (currency, dates, phone, etc.)
- ⚙️ Publishable config for global defaults
- 🎭 Blade component for non-Nova use
- 🎁 Auto-flatten single-item arrays (no more "#1" suffixes!)
- 🔢 Customizable or skippable array indices
- 📦 Conditional flattening based on array size
- 🗂️ Simplified nested array iteration
No more seeing "Item #1" when you only have one item! Use flattenSingleArrays() to automatically extract single items:
JsonToKeyvalue::make($data, 'User')
->flattenSingleArrays(true)
->toFields();Transform complex nested structures with one simple method:
// Before
collect($this->data)->flatMap(function ($value, $key) {
$label = ucwords(str_replace('_', ' ', $key));
$data = is_array($value) && isset($value[0]) ? $value[0] : $value;
return JsonToKeyvalue::make($data, $label)->flattenNested(true)->toFields();
})->toArray()
// After
JsonToKeyvalue::fromNestedArray($this->data)Skip indices entirely or customize their format:
// Skip completely
->skipArrayIndices(true)
// Use parentheses
->arrayIndexFormat(' (%d)')
// Use brackets
->arrayIndexFormat(' [%d]')Conditionally flatten arrays and control when to process large datasets:
->maxArraySize(10) // Only process arrays with ≤10 items- PHP 8.1+
- Laravel Nova (not included - must be installed separately)
composer require provydon/json-to-keyvalueThe simplest usage:
use Provydon\JsonToKeyvalue\JsonToKeyvalue;
public function fields(Request $request)
{
return [
JsonToKeyvalue::make($this->metadata, 'Metadata')
->toFields()
];
}That's it! Your nested JSON is now flattened and displayed as readable key-value pairs.
You can chain methods to customize the output:
use Provydon\JsonToKeyvalue\JsonToKeyvalue;
public function fields(Request $request)
{
return JsonToKeyvalue::make($this->metadata, 'Metadata')
->skip(['password'])
->excludeSuffixes(['_error'])
->formatters([
'amount' => fn($value) => '₦' . number_format($value, 2)
])
->toFields();
}Why use the class?
- ✨ Clean, readable, chainable methods
- 🎯 Type hints and IDE autocomplete
- 🔧 Easier to test and maintain
- 📦 Can use
toArray()for non-Nova contexts
The global helper function is still available but less recommended:
public function fields(Request $request)
{
return [
json_to_keyvalue_fields($this->metadata, 'Metadata', [
'skip' => ['password'],
'exclude_suffixes' => ['_error']
])
];
}For displaying key-value pairs outside of Nova:
<x-keyvalue-display
:data="$jsonData"
label="User Details"
:config="['skip' => ['password']]"
/>| Method | Parameters | Description |
|---|---|---|
make($data, $label) |
data, label | Create new instance |
fromNestedArray($data, $formatter) |
array, ?callable | Static method to iterate nested arrays |
skip($keys) |
array | Skip specific keys |
excludeSuffixes($suffixes) |
array | Exclude keys by suffix |
excludePrefixes($prefixes) |
array | Exclude keys by prefix |
flattenNested($bool) |
boolean | Enable/disable flattening |
nestedSeparator($sep) |
string | Set separator for nested keys |
flattenSingleArrays($bool) |
boolean | Auto-extract single-item arrays |
skipArrayIndices($bool) |
boolean | Skip adding array indices to labels |
arrayIndexFormat($format) |
string | Customize array index format (sprintf) |
maxArraySize($size) |
?int | Only flatten arrays below this size |
itemLabel($label) |
string | Label for array items |
labels($labels) |
array | Custom field labels |
formatters($formatters) |
array | Custom formatters |
lookups($lookups) |
array | Database lookups |
config($config) |
array | Merge config array |
toFields() |
- | Return Nova fields |
toArray() |
- | Return plain arrays |
Skip specific keys
JsonToKeyvalue::make($data, 'Payment Info')
->skip(['cvv', 'password', 'secret_key'])
->toFields();Exclude by suffix/prefix
JsonToKeyvalue::make($data, 'Response')
->excludeSuffixes(['_error', '_debug', '_internal'])
->excludePrefixes(['temp_', 'cache_'])
->toFields();Custom labels
JsonToKeyvalue::make($data, 'User')
->labels([
'dob' => 'Date of Birth',
'phone_number' => 'Phone',
'created_at' => 'Member Since'
])
->toFields();Custom formatters
JsonToKeyvalue::make($data, 'Transaction')
->formatters([
'amount' => fn($value) => '₦' . number_format($value, 2),
'created_at' => fn($value) => \Carbon\Carbon::parse($value)->format('M d, Y'),
'status' => fn($value) => strtoupper($value)
])
->toFields();Database lookups
JsonToKeyvalue::make($data, 'Order')
->lookups([
'user_id' => [
'model' => \App\Models\User::class,
'field' => 'id',
'display' => 'name',
'fallback' => 'user_id'
],
'product_id' => [
'model' => \App\Models\Product::class,
'field' => 'id',
'display' => 'title'
]
])
->toFields();Nested array handling
JsonToKeyvalue::make($data, 'Config')
->flattenNested(true)
->nestedSeparator(' > ')
->toFields();Multiple items
$orders = [
['id' => 1, 'total' => 5000],
['id' => 2, 'total' => 3000]
];
JsonToKeyvalue::make($orders, 'Order')
->itemLabel('Order')
->toFields();Auto-flatten single-item arrays
// If your data has single-item arrays like [['name' => 'John']]
// This will extract the item without showing "Item #1"
JsonToKeyvalue::make($data, 'User')
->flattenSingleArrays(true)
->toFields();Skip array indices
// Removes "#1", "#2" suffixes from labels
JsonToKeyvalue::make($items, 'Items')
->skipArrayIndices(true)
->toFields();Custom array index format
// Customize how array indices are displayed
JsonToKeyvalue::make($items, 'Item')
->arrayIndexFormat(' (%d)') // Item (1), Item (2)
->toFields();
// Or use brackets
JsonToKeyvalue::make($items, 'Item')
->arrayIndexFormat(' [%d]') // Item [1], Item [2]
->toFields();Conditional flattening by size
// Only process arrays with 10 or fewer items
JsonToKeyvalue::make($data, 'Large Dataset')
->maxArraySize(10)
->toFields();Nested array iteration
// The old way
Panel::make('Details', $this->data
? collect($this->data)->flatMap(function ($value, $key) {
$label = ucwords(str_replace('_', ' ', $key));
$data = is_array($value) && isset($value[0]) ? $value[0] : $value;
return JsonToKeyvalue::make($data, $label)->flattenNested(true)->toFields();
})->toArray()
: []
),
// The new way ✨
Panel::make('Details',
$this->data ? JsonToKeyvalue::fromNestedArray($this->data) : []
),
// With custom label formatter
Panel::make('Details',
$this->data
? JsonToKeyvalue::fromNestedArray($this->data, fn($key) => strtoupper($key))
: []
),Complete example
use Provydon\JsonToKeyvalue\JsonToKeyvalue;
use Provydon\JsonToKeyvalue\Formatters;
JsonToKeyvalue::make($data, 'Payment Details')
->skip(['cvv', 'secret_key'])
->excludeSuffixes(['_error', '_debug'])
->flattenNested(true)
->nestedSeparator(' → ')
->labels([
'transaction_ref' => 'Reference',
'created_at' => 'Date'
])
->formatters([
'amount' => Formatters::currency('₦', 2),
'created_at' => Formatters::datetime('M d, Y g:i A')
])
->lookups([
'user_id' => [
'model' => \App\Models\User::class,
'field' => 'id',
'display' => 'email',
'fallback' => 'user_id'
]
])
->toFields();Using toArray() for non-Nova contexts
$data = JsonToKeyvalue::make($payment, 'Payment')
->skip(['internal_id'])
->formatters(['amount' => Formatters::currency('$')])
->toArray();The package includes ready-to-use formatters:
use Provydon\JsonToKeyvalue\JsonToKeyvalue;
use Provydon\JsonToKeyvalue\Formatters;
JsonToKeyvalue::make($data, 'Transaction')
->formatters([
'amount' => Formatters::currency('₦', 2),
'created_at' => Formatters::date('M d, Y'),
'updated_at' => Formatters::datetime('M d, Y g:i A'),
'is_active' => Formatters::boolean('Active', 'Inactive'),
'status' => Formatters::uppercase(),
'name' => Formatters::titleCase(),
'phone' => Formatters::phone('+234'),
'description' => Formatters::truncate(100),
'discount' => Formatters::percentage(2),
'file_size' => Formatters::fileSize(),
'metadata' => Formatters::json(pretty: true),
'type' => Formatters::enumLabel([
'pending' => 'Pending Payment',
'completed' => 'Completed'
])
])
->toFields();currency($symbol, $decimals)- Format numbers as currencydate($format)- Format datesdatetime($format)- Format date and timeboolean($trueLabel, $falseLabel)- Convert boolean to textuppercase()- Convert to uppercaselowercase()- Convert to lowercasetitleCase()- Convert to title casephone($countryCode)- Format phone numberstruncate($length, $ending)- Truncate long textpercentage($decimals)- Format as percentagefileSize()- Convert bytes to human-readable sizejson($pretty)- Format as JSONenumLabel($labels)- Map enum values to labels
Publish the config file:
php artisan vendor:publish --tag=json-to-keyvalue-configSet global defaults in config/json-to-keyvalue.php:
return [
'exclude_suffixes' => ['_error', '_debug'],
'exclude_prefixes' => ['temp_'],
'flatten_nested' => true,
'nested_separator' => ' → ',
'flatten_single_arrays' => false,
'skip_array_indices' => false,
'array_index_format' => ' #%d',
'max_array_size' => null,
'skip' => [],
'labels' => [],
'formatters' => [],
'lookups' => [],
];Publish the views:
php artisan vendor:publish --tag=json-to-keyvalue-viewsUse in Blade templates:
<x-keyvalue-display
:data="$user->metadata"
label="User Metadata"
:config="[
'skip' => ['password'],
'formatters' => [
'created_at' => fn($v) => $v->format('M d, Y')
]
]"
/>use Provydon\JsonToKeyvalue\JsonToKeyvalue;
use Provydon\JsonToKeyvalue\Formatters;
JsonToKeyvalue::make($metadata, 'User Metadata')
->skip(['password', 'token', 'api_key'])
->excludeSuffixes(['_error', '_internal', '_debug'])
->excludePrefixes(['temp_', 'cache_'])
->flattenNested(true)
->nestedSeparator(' → ')
->itemLabel('Metadata')
->labels([
'first_name' => 'First Name',
'last_name' => 'Last Name'
])
->formatters([
'created_at' => Formatters::datetime(),
'amount' => Formatters::currency('₦'),
'is_active' => Formatters::boolean()
])
->toFields();For complex configurations, you can pass an array:
JsonToKeyvalue::make($data, 'Details')
->config([
'skip' => ['password'],
'exclude_suffixes' => ['_error'],
'formatters' => [
'amount' => Formatters::currency('$')
]
])
->toFields();The package provides array_flatten_with_keys() for general use:
$flat = array_flatten_with_keys([
'user' => [
'name' => 'John',
'address' => ['city' => 'Lagos']
]
], '', ' → ');
// Result: ['user → name' => 'John', 'user → address → city' => 'Lagos']Run the tests:
composer testOr:
./vendor/bin/phpunitThis package uses Laravel Pint for code formatting:
Format code:
composer formatCheck formatting without fixing:
composer format:testOr run Pint directly:
./vendor/bin/pintTo test this package locally in another Laravel project before publishing to Packagist:
Add to your project's composer.json:
"repositories": [
{
"type": "path",
"url": "../json-to-keyvalue"
}
],
"require": {
"provydon/json-to-keyvalue": "*"
}Then run:
composer update provydon/json-to-keyvalue- Push your code to GitHub
- Go to packagist.org and sign in
- Click "Submit" and paste your GitHub repository URL
- Packagist will auto-update on each GitHub push (configure webhook for automation)
Users can then install via:
composer require provydon/json-to-keyvalueMIT


