JSON Tree Editor: Recursive Rendering and Two-Way Sync#

Working with complex JSON data recently, plain text editing wasn’t cutting it. I needed a visual tree editor where I could add, delete, and modify nodes directly. So I built one. Here’s the core approach.

Data Model for Tree Structure#

Converting JSON to a tree requires a unified node model:

interface TreeNode {
  id: string              // Unique identifier (React key + node lookup)
  key: string             // Key name
  value: any              // Value (primitive stored directly, object/array as reference)
  type: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'
  children?: TreeNode[]   // Child nodes (only for object/array)
  isExpanded?: boolean    // Collapse state
}

Why the id field?

React’s key can’t use key names (might duplicate) or values (objects lack unique IDs). A random string works fine for rendering identity.

JSON → Tree Recursive Parsing#

Core function:

function parseJSONToTree(obj: any, key: string = 'root'): TreeNode {
  const id = Math.random().toString(36).substr(2, 9)

  // null needs special handling (typeof null === 'object')
  if (obj === null) {
    return { id, key, value: null, type: 'null' }
  }

  // Object: recursively process each property
  if (typeof obj === 'object' && !Array.isArray(obj)) {
    return {
      id,
      key,
      value: obj,
      type: 'object',
      isExpanded: true,
      children: Object.entries(obj).map(([k, v]) => parseJSONToTree(v, k))
    }
  }

  // Array: recursively process each element, key is [0], [1]...
  if (Array.isArray(obj)) {
    return {
      id,
      key,
      value: obj,
      type: 'array',
      isExpanded: true,
      children: obj.map((v, i) => parseJSONToTree(v, `[${i}]`))
    }
  }

  // Primitive: return directly
  return { id, key, value: obj, type: typeof obj as any }
}

Key points:

  1. null trap: typeof null === 'object', must check first
  2. Array keys: Use [0], [1] format for clarity
  3. Expand state: Default all expanded for easier editing

Tree → JSON Reverse Conversion#

After editing, export JSON with reverse conversion:

function treeToJSON(node: TreeNode): any {
  if (node.type === 'null') return null

  if (node.type === 'object') {
    const result: any = {}
    node.children?.forEach(child => {
      result[child.key] = treeToJSON(child)
    })
    return result
  }

  if (node.type === 'array') {
    return node.children?.map(child => treeToJSON(child)) || []
  }

  return node.value
}

Why not JSON.stringify?

The tree’s value field stores original references. Direct stringify would hit circular reference issues. Recursive rebuild gives precise output control.

Two-Way Sync: Tree Edit → JSON Text#

When users edit nodes in tree view, sync the JSON text on the left:

const editNode = (id: string, newValue: any) => {
  // 1. Recursively find and update node
  const updateNode = (node: TreeNode): TreeNode => {
    if (node.id === id) {
      const newType = newValue === null ? 'null' : typeof newValue as any
      return { ...node, value: newValue, type: newType }
    }
    if (node.children) {
      return { ...node, children: node.children.map(updateNode) }
    }
    return node
  }

  // 2. Update tree
  const newTree = updateNode(tree)
  setTree(newTree)

  // 3. Generate JSON, update text area
  const newJson = treeToJSON(newTree)
  setJsonInput(JSON.stringify(newJson, null, 2))
}

Recursive search over Map because tree depth is usually limited (JSON nesting has practical limits), and recursive code is clearer.

Node Collapse/Expand#

Toggle needs recursive isExpanded update:

const toggleNode = (id: string) => {
  const updateNode = (node: TreeNode): TreeNode => {
    if (node.id === id) {
      return { ...node, isExpanded: !node.isExpanded }
    }
    if (node.children) {
      return { ...node, children: node.children.map(updateNode) }
    }
    return node
  }
  setTree(updateNode(tree))
}

Performance optimization:

For large trees (thousands of nodes), recursive traversal every time gets slow. Use a Map for node references:

// Build index on init
const nodeMap = new Map<string, TreeNode>()
function buildMap(node: TreeNode) {
  nodeMap.set(node.id, node)
  node.children?.forEach(buildMap)
}

// Direct reference lookup O(1)
const toggleNode = (id: string) => {
  const node = nodeMap.get(id)
  if (node) node.isExpanded = !node.isExpanded
  setTree({ ...tree }) // Trigger re-render
}

But JSON trees usually aren’t huge. Recursive is fine.

Node Deletion Edge Cases#

Deleting nodes requires handling parent’s children array:

const deleteNode = (id: string) => {
  const removeNode = (node: TreeNode): TreeNode | null => {
    if (node.id === id) return null // Found target, null means delete

    if (node.children) {
      const filtered = node.children
        .map(removeNode)
        .filter((n): n is TreeNode => n !== null) // Filter out nulls
      return { ...node, children: filtered }
    }
    return node
  }

  const newTree = removeNode(tree)
  if (newTree) {
    setTree(newTree)
    setJsonInput(JSON.stringify(treeToJSON(newTree), null, 2))
  }
}

Why check newTree for null?

If user deletes root node, the whole tree is gone. Either prevent deletion or reset to empty object {}.

Type Coloring and Visual Feedback#

Different types get different colors for instant recognition:

<span className={`text-sm ${
  node.type === 'string' ? 'text-green-500' :
  node.type === 'number' ? 'text-amber-500' :
  node.type === 'boolean' ? 'text-purple-500' :
  node.type === 'null' ? 'text-gray-500' :
  'text-text-secondary'
}`}>
  {node.type === 'string' ? `"${node.value}"` :
   node.type === 'null' ? 'null' :
   node.type === 'object' ? `{${node.children?.length || 0} items}` :
   node.type === 'array' ? `[${node.children?.length || 0} items]` :
   String(node.value)}
</span>

Objects and arrays show child count for quick structure overview.

Real-World Use Cases#

This JSON tree editor works well for:

  • Config file editing: Visually modify complex configurations
  • API data debugging: Quickly locate and edit nested fields
  • Teaching demos: Intuitively show JSON structure

Try it: JSON Tree Editor


Related: JSON Formatter | JSON Diff