json-edit-react
A React component for editing or viewing JSON/object data
Features include:
- edit individual values, or whole objects as JSON text
- fine-grained control over which elements can be edited, deleted, or added to
- customisable UI, through simple, pre-defined themes, or specific CSS overrides
- self-contained — rendered with plain HTML/CSS, so no dependance on external UI libraries
- provide your own custom component to integrate specialised UI for certain data.
Explore the Demo
Installation
npm i json-edit-react
or
yarn add json-edit-react
Implementation
import { JsonEditor } from 'json-edit-react'
<JsonEditor
data={ myData }
onUpdate={ ({newData} ) => {
}}
{ ...otherProps } />
You are responsible for maintaining the data state — in your onUpdate
function, use the newData
property to set data
, which should update inside the editor.
Usage
(for end user)
It's pretty self explanatory (click the "edit" icon to edit, etc.), but there are a few not-so-obvious ways of interacting with the editor:
- Double-click a value (or a key) to edit it
- When editing a string, use
Cmd/Ctrl/Shift-Enter
to add a new line (Enter
submits the value) - It's the opposite when editing a full object/array node (which you do by clicking "edit" on an object or array value) —
Enter
for new line, and Cmd/Ctrl/Shift-Enter
for submit Escape
to cancel editing- When clicking the "clipboard" icon, holding down
Cmd/Ctrl
will copy the path to the selected node rather than its value
Props overview
The only required value is data
.
prop | type | default | description |
---|
data | object|array | | The data to be displayed / edited |
rootName | string | "data" | A name to display in the editor as the root of the data object. |
onUpdate | UpdateFunction | | A function to run whenever a value is updated (edit, delete or add) in the editor. See Update functions. |
onUpdate | UpdateFunction | | A function to run whenever a value is edited. |
onDelete | UpdateFunction | | A function to run whenever a value is deleted. |
onAdd | UpdateFunction | | A function to run whenever a new property is added. |
enableClipboard | boolean|CopyFunction | true | Whether or not to enable the "Copy to clipboard" button in the UI. If a function is provided, true is assumed and this function will be run whenever an item is copied. |
indent | number | 4 | Specify the amount of indentation for each level of nesting in the displayed data. |
collapse | boolean|number|FilterFunction | false | Defines which nodes of the JSON tree will be displayed "opened" in the UI on load. If boolean , it'll be either all or none. A number specifies a nesting depth after which nodes will be closed. For more fine control a function can be provided — see Filter functions. |
restrictEdit | boolean|FilterFunction | false | If false , no editing is permitted. A function can be provided for more specificity — see Filter functions |
restrictDelete | boolean|FilterFunction | false | As with restrictEdit but for deletion |
restrictAdd | boolean|FilterFunction | false | As with restrictEdit but for adding new properties |
restrictTypeSelection | boolean|DataType[]|TypeFilterFunction | false | For restricting the data types the user can select. This varies slightly from the above restrictions in that the value (or output of the TypeFilterFunction ) can be a list of data types or a boolean . |
keySort | boolean|CompareFunction | false | If true , object keys will be ordered (using default JS .sort() ). A compare function can also be provided to define sorting behaviour. |
showArrayIndices | boolean | true | Whether or not to display the index (as a property key) for array elements. |
showCollectionCount | boolean|"when-closed" | true | Whether or not to display the number of items in each collection (object or array). |
defaultValue | any | null | When a new property is added, it is initialised with this value. |
stringTruncate | number | 250 | String values longer than this many characters will be displayed truncated (with ... ). The full string will always be visible when editing. |
translations | LocalisedStrings object | { } | UI strings (such as error messages) can be translated by passing an object containing localised string values (there are only a few). See Localisation |
theme | string|ThemeObject|[string, ThemeObject] | "default" | Either the name of one of the built-in themes, or an object specifying some or all theme properties. See Themes. |
className | string | | Name of a CSS class to apply to the component. In most cases, specifying theme properties will be more straightforward. |
icons | {[iconName]: JSX.Element, ... } | { } | Replace the built-in icons by specifying them here. See Themes. |
minWidth | number|string (CSS value) | 250 | Minimum width for the editor container. |
maxWidth | number|string (CSS value) | 600 | Maximum width for the editor container. |
customNodeDefinitions | CustomNodeDefinition[] | | You can provide customised components to override specific nodes in the data tree, according to a condition function. See see Custom nodes for more detail. |
Update functions
A callback to be executed whenever a data update (edit, delete or add) occurs can be provided. You might wish to use this to update some state, or make an API call, for example. If you want the same function for all updates, then just the onUpdate
prop is sufficient. However, should you require something different for editing, deletion and addition, then you can provide separate Update functions via the onEdit
, onDelete
and onAdd
props.
The function will receive the following object as a parameter:
{
newData,
currentData,
newValue,
currentValue,
name,
path
}
The function needn't return anything, but if it returns false
, it will be considered an error, in which case an error message will displayed in the UI and the internal data state won't actually be updated. If the return value is a string
, this will be the error message displayed (i.e. you can define your own error messages for updates).
Copy function
A similar callback is executed whenever an item is copied to the clipboard (if passed to the enableClipboard
prop), but with a different input parameter:
key
path
value
type
stringValue
Since there is very little user feedback when clicking "Copy", a good idea would be to present some kind of notification in this callback.
Filter functions
You can control which nodes of the data structure can be edited, deleted, or added to, or have their data type changed, by passing Filter functions. These will be called on each property in the data and the attribute will be enforced depending on whether the function returns true
or false
(true
means cannot be edited).
The function receives the following object:
{
key,
path,
level,
value,
size
}
A Filter function is available for the collapse
prop as well, so you can have your data appear with deeply-nested collections opened up, while collapsing everything else, for example.
For restricting data types, the (Type) filter function is slightly more sophisticated. The input is the same, but the output can be either a boolean
(which would restrict the available types for a given node to either all or none), or an array of data types to be restricted to. The available values are:
"string"
"number"
"boolean"
"null"
"object"
"array"
There is no specific restriction function for editing object key names, but they must return true
for both restrictEdit
and restrictDelete
(and restrictAdd
for collections), since changing a key name is equivalent to deleting a property and adding a new one.
Using all these restriction filters together can allow you to enforce a reasonably sophisticated data schema.
Examples
- A good case would be ensure your root node is not directly editable:
restrictEdit = { ({ level }) => level === 0 }
- Don't let the
id
field be edited:
restrictEdit = { ({ key }) => key === "id" }
- Only individual properties can be deleted, not objects or arrays:
restrictDelete = { ({ size }) => size !== null }
- The only collections that can have new items added are the "address" object and the "users" array:
restrictAdd = { ({ key }) => key !== "address" && key !== "users" }
- Multiple type restrictions:
string
values can only be changed to strings or objects (for nesting)null
is not allowed anywhereboolean
values must remain boolean- data nested below the "user" field can be any simple property (i.e. not objects or arrays), and doesn't have to follow the above rules (except no "null")
restrictTypeSelection = { ({ path, value }) => {
if (path.includes('user')) return ['string', 'number', 'boolean']
if (typeof value === 'boolean') return false
if (typeof value === 'string') return ['string', 'object']
return ['string', 'number', 'boolean', 'array', 'object']
} }
Themes
There is a small selection of built-in themes (as seen in the Demo app). In order to use one of these, just pass the name into the theme
prop (although realistically, these exist more to showcase the capabilities — I'm open to better built-in themes, so feel free to create an issue with suggestions). The available themes are:
default
githubDark
githubLight
monoDark
monoLight
candyWrapper
psychedelic
However, you can pass in your own theme object, or part thereof. The theme structure is as follows (this is the "default" theme definition):
{
displayName: 'Default',
fragments: { edit: 'rgb(42, 161, 152)' },
styles: {
container: {
backgroundColor: '#f6f6f6',
fontFamily: 'monospace',
},
property: '#292929',
bracket: { color: 'rgb(0, 43, 54)', fontWeight: 'bold' },
itemCount: { color: 'rgba(0, 0, 0, 0.3)', fontStyle: 'italic' },
string: 'rgb(203, 75, 22)',
number: 'rgb(38, 139, 210)',
boolean: 'green',
null: { color: 'rgb(220, 50, 47)', fontVariant: 'small-caps', fontWeight: 'bold' },
input: ['#292929', { fontSize: '90%' }],
inputHighlight: '#b3d8ff',
error: { fontSize: '0.8em', color: 'red', fontWeight: 'bold' },
iconCollection: 'rgb(0, 43, 54)',
iconEdit: 'edit',
iconDelete: 'rgb(203, 75, 22)',
iconAdd: 'edit',
iconCopy: 'rgb(38, 139, 210)',
iconOk: 'green',
iconCancel: 'rgb(203, 75, 22)',
},
}
The styles
property is the main one to focus on. Each key (property
, bracket
, itemCount
) refers to a part of the UI. The value for each key is either a string
, in which case it is interpreted as the colour (or background colour in the case of container
and inputHighlight
), or a full CSS style object for fine-grained definition. You only need to provide properties you wish to override — all unspecified ones will fallback to either the default theme, or another theme that you specify as the "base".
For example, if you want to use the "githubDark" theme, but just change a couple of small things, you'd specify something like this:
theme={[
'githubDark',
{
iconEdit: 'grey',
boolean: { color: 'red', fontStyle: 'italic', fontWeight: 'bold', fontSize: '80%' },
},
]}
Which would change the "Edit" icon and boolean values from this:
into this:
Or you could create your own theme from scratch and overwrite the whole theme object.
So, to summarise, the theme
prop can take either:
- a theme name e.g.
"candyWrapper"
- a theme object:
- can be structured as above with
fragments
, styles
, displayName
etc., or just the styles
part (at the root level)
- a theme name and an override object in an array, i.e.
[ "<themeName>, {...overrides } ]
You can play round with live editing of the themes in the Demo app by selecting "Edit this theme!" from the "Demo data" selector.
Fragments
The fragments
property above is just a convenience to allow repeated style "fragments" to be defined once and referred to using an alias. For example, if you wanted all your icons to be blue and slightly larger and spaced out, you might define a fragment like so:
fragments: { iconAdjust: { color: "blue", fontSize: "110%", marginRight: "0.6em" }}
Then in the theme object, just use:
{
...,
iconEdit: "iconAdjust",
iconDelete: "iconAdjust",
iconAdd: "iconAdjust",
iconCopy: "iconAdjust",
}
Then, when you want to tweak it later, you only need to update it in one place!
Fragments can also be mixed with additional properties, and even other fragments, like so:
iconEdit: [ "iconAdjust", "anotherFragment", { marginLeft: "1em" } ]
A note about sizing and scaling
Internally, all sizing and spacing is done in em
s, never px
. This makes scaling a lot easier — just change the font-size of the main container (either via the className
prop or in the container
prop of the theme) (or its parent), and watch the whole component scale accordingly.
Icons
The default icons can be replaced, but you need to provide them as React/HTML elements. Just define any or all of them within the icons
prop, keyed as follows:
icons={{
add: <YourIcon />
edit: <YourIcon />
delete: <YourIcon />
copy: <YourIcon />
ok: <YourIcon />
cancel: <YourIcon />
chevron: <YourIcon />
}}
The Icon components will need to have their own styles defined, as the theme styles won't be added to the custom elements.
Localisation
Localise your implementation by passing in a translations
object to replace the default strings. The keys and default (English) values are as follows:
{
ITEM_SINGLE: '{{count}} item',
ITEMS_MULTIPLE: '{{count}} items',
KEY_NEW: 'Enter new key',
ERROR_KEY_EXISTS: 'Key already exists',
ERROR_INVALID_JSON: 'Invalid JSON',
ERROR_UPDATE: 'Update unsuccessful',
ERROR_DELETE: 'Delete unsuccessful',
ERROR_ADD: 'Adding node unsuccessful',
DEFAULT_STRING: 'New data!',
DEFAULT_NEW_KEY: 'key',
}
Custom nodes
You can replace certain nodes in the data tree with your own custom components. An example might be for an image display, or a custom date editor, or just to add some visual bling. See the "Custom Nodes" data set in the interactive demo to see it in action.
Custom nodes are provided in the customNodeDefinitions
prop, as an array of objects of following structure:
{
condition,
element,
props,
hideKey,
showInTypesSelector,
name
defaultValue,
editable
}
The condition
is just a Filter function, with the same input parameters (key
, path
, level
, value
, size
), and element
is a React component. Every node in the data structure will be run through each condition function, and any that match will be replaced by your custom component. Note that if a node matches more than one custom definition conditions (from multiple components), the first one will be used, so place them in the array in priority order.
The component will receive all the same props as a standard node component (see codebase), but you can pass additional props to your component if required through the props
object.
By default, your component will be presented to the right of the property key it belongs to, like any other value. However, you can hide the key itself by setting hideKey: true
, and the custom component will take the whole row. (See the "Presented by" box in the "Custom Nodes" data set for an example.)
You can allow users to create new instances of your special nodes by selecting them as a "Type" in the types selector when editing/adding values. Set showInTypesSelector: true
to enable this. However, if this is enabled you need to also provide a name
(which is what the user will see in the selector) and a defaultValue
which is the data that is inserted when the user selects this "type". (The defaultValue
must return true
if passed through the condition
function in order for it to be immediately displayed using your custom component.)
Lastly, you can specify whether or not the data inside the node can be edited using the standard editor, with the editable
prop (default: true
). If your component includes its own editing interface (e.g. a Date Picker), you might want to disable the standard editor.
Undo functionality
Even though Undo/Redo functionality is probably desirable in most cases, this is not built in to the component, for two main reasons:
- It would involve too much additional UI and I didn't want this component becoming opinionated about the look and feel beyond the essentials (which are mostly customisable/style-able anyway)
- It is quite straightforward to implement using existing libraries. I've used use-undo in the Demo, which is working well.
Issues, bugs, suggestions?
Please open an issue: https://github.com/CarlosNZ/json-edit-react/issues
Roadmap
The main features I'd like to introduce are:
- JSON Schema validation. We can currently specify a reasonable degree of control over what can be edited using Filter functions with the restriction props, but I'd like to go a step further and be able to pass in a JSON Schema and have the data be automatically validated against it, with the results reflected in the UI. This would allow control over data types and prevent missing properties, something that is not currently possible.
- Visibility filter function — hide properties from the UI completely based on a Filter function. This should arguably be done outside the component though (filter the data upstream), so would be less of a priority (though it would be fairly simple to implement, so 🤷♂️)
- Search — allow the user to narrow the list of visible keys with a simple search input. This would be useful for very large data objects, but is possibly getting a bit too much in terms of opinionated UI, so would need to ensure it can be styled easily. Perhaps it would be better if the "Search" input was handled outside this package, and we just accepted a "search" string prop?
Inspiration
This component is heavily inspired by react-json-view, a great package that I've used in my own projects. However, it seems to have been abandoned now, and requires a few critical fixes, so I decided to create my own from scratch and extend the functionality while I was at it.
Changelog
- 1.2.0: Allow editing of Custom nodes
- 1.1.0: Don't manage data state within component
- 1.0.0:
- Custom nodes
- Allow editing of keys
- Option to define restrictions on data type selection
- Option to hide array/object item counts
- Improve keyboard interaction
- 0.9.6: Performance improvement by not processing child elements if not visible
- 0.9.4:
- Layout improvements
- Better internal handling of functions in data
- 0.9.3: Bundle as ES6 module
- 0.9.1: Export more Types from the package
- 0.9.0: Initial release