JSON Viewer

Free

Expandable tree over any value — the panel a webhook inspector, an admin tool or a debug drawer needs. Tells NaN from null, a Map from an empty object and a cycle from a shared reference. Masks secrets in the value as well as on screen, lists accessors without invoking them, discloses the own properties `Object.entries` silently drops, holds a total row ceiling rather than only a per-container one, and carries a full tree keyboard model with copy-path and copy-value.

</>sv-json-viewer jsontreedebugredactsecrets

Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.

import { SvJsonViewerComponent } from '@styloviz/display';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

A webhook payload, opened two levels

example.htmlAngular
<sv-json-viewer [value]="payload()" rootLabel="payload" [expandDepth]="2" size="sm" />

A very large array, capped

example.htmlAngular
<sv-json-viewer [value]="rows()" [maxChildren]="50" />
<!-- The cap is counted and shown as "… N more" rather than silently dropping rows. -->

Masking secrets before the screenshot

example.htmlAngular
import { secretKeyRedactor } from '@styloviz/display';
// redact = secretKeyRedactor — matches password, token, api-key, authorization, cvv…

<sv-json-viewer [value]="request()" [redact]="redact" redactedLabel="[redacted]" />
<!-- The mask is the row's VALUE too, so copy and nodeClick cannot leak it. -->

A redactor of your own

example.htmlAngular
readonly redact: JsonRedactor = ({ path, key }) =>
  path.startsWith('$.headers') || key === 'pan';
// path and kind are there so a whole subtree can be masked by position,
// rather than by naming every leaf inside it.

Reading accessors, on request

example.htmlAngular
<sv-json-viewer [value]="session()" [evaluateGetters]="true" />
<!-- Off by default: opening a panel to look at an object is not consent to run
     its getters. On, each is read in its own try/catch. -->

A ceiling that nesting cannot get past

example.htmlAngular
<sv-json-viewer [value]="config()" [expandDepth]="3" [maxRows]="500" />
<!-- 40 containers of 40 children is 1,640 rows, all inside maxChildren: 100. -->

Driven from the host

example.htmlAngular
@ViewChild(SvJsonViewerComponent) viewer!: SvJsonViewerComponent;
// viewer.expandAll() / viewer.collapseAll() — or press * on the focused row.

When to use

  • Showing a raw API response, webhook payload, event body, config blob or reducer state.
  • Any admin or support screen where the question is "what did we actually receive?".
  • Anywhere the panel could end up in a screenshot, a screen recording or a support ticket — pass `redact` and the secrets never reach the DOM or the clipboard.
  • Inspecting live objects rather than parsed JSON: class instances, things with accessors, values you have no reason to trust enough to execute.
  • For source code with syntax highlighting, use Code Block. For a hierarchy of app entities, use Tree View.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
valueunknownundefinedThe value to show. Anything at all — this is not restricted to parsed JSON.
rootLabelstring'root'Name of the root node.
expandDepthnumber1Auto-expand to this depth on load. 1, not 0 and not Infinity. A collapsed root shows nothing and reads as broken; a fully expanded 4 MB response locks the tab.
maxChildrennumber100Maximum children rendered per container before a "… N more" row. A 50,000-element array is not a rendering problem to solve with patience: it is a row per element, and the browser stops responding. The cap is visible and counted rather than silent — a viewer that quietly drops rows is worse than one that shows fewer.
maxRowsnumber2000Maximum rows rendered in total, across the whole tree. `maxChildren` IS NOT A CEILING, IT IS A WIDTH LIMIT. A hundred children each with a hundred children is ten thousand rows, and every one of them passed the per-container cap. Nesting multiplies, and `expandAll()` / `*` reaches all of it at once — which is how a debug panel becomes the reason the tab stopped responding. This is the actual ceiling, and the walk stops *at* it rather than building the rest and slicing: the cost of a payload nobody can read is paid in the traversal, not just in the DOM. Reaching it prints a row saying so, because a tree that silently ends is a tree the reader believes they finished.
sizeJsonViewerSize'md'Type scale and row density.
sortKeysbooleanfalseSort object keys alphabetically instead of insertion order.
quoteStringsbooleantrueRender strings with quotes and escapes, as JSON would.
showCopybooleantrueShow the copy-path / copy-value action on the focused row.
redactJsonRedactor | nullnullMasks matching nodes before they are rendered. A JSON VIEWER IS WHERE AN API KEY ENDS UP IN A SCREENSHOT. Bearer tokens, card numbers and session cookies all arrive inside perfectly ordinary payloads, and the person who pastes one into a debug drawer is not thinking about the screen recording. Return `true` from the predicate and the row prints `redactedLabel` instead of the value, stops being expandable, and — the part that matters — carries the MASK as its value, so "Copy value" and `nodeClick` cannot hand the secret out either. Redaction that only changed the pixels would be redaction the clipboard walks straight past. Redacting a container masks the entire subtree: the walk does not descend, so nothing underneath is rendered, counted or copyable. There is no default. A component cannot know which of your fields is a secret, and one that guessed would hide data the reader needed. Pass the exported `secretKeyRedactor` for the usual suspects.
redactedLabelstring'[redacted]'What a masked row shows in place of its value.
evaluateGettersbooleanfalseRead accessor properties instead of listing them unevaluated. OFF, BECAUSE LOOKING AT AN OBJECT IS NOT CONSENT TO RUN ITS CODE. A getter can hit the network, mutate state, throw, or be an intentionally expensive lazy field; `Object.entries` calls every one of them just to enumerate. Off, each accessor gets a row that says it is a getter and was not read. On, each is called inside a `try` and a thrower renders its message rather than taking the panel down.
ariaLabelstring'JSON'Accessible name for the tree.
customClassstring''Extra classes on the root element.

Outputs

EventPayload typeDescription
nodeClickJsonNodeClickA row was activated (click or Enter).
copiedJsonCopyEventSomething reached the clipboard. Reports the path, which button was pressed and whether the row was masked, so a host can keep an audit trail without logging a second copy of the value it just tried to protect.
copyAuditedSvSensitiveCopyEventSomething reached the clipboard — the audit-safe twin of `copied`. Carries the source, the path, whether the copied text contained a mask and when, and never the text itself, so it can be logged as-is.

Customization & theming

  • `svDiffValues()` from `@styloviz/core` compares two JSON values leaf by leaf and returns `{ path, kind, before, after }` — the shared diff engine, so a JSON diff is not a private half-implementation of one. Its `ignore` predicate takes the PATH rather than the key, so `meta.updatedAt` can be excluded while a top-level `updatedAt` is not.
  • Control the initial shape with `expandDepth`, the per-container width with `maxChildren` and the total size with `maxRows`; `expandAll()` / `collapseAll()` are public, and `*` on a focused row expands everything.
  • `size` (sm / md), `sortKeys` for alphabetical object keys, `quoteStrings` to render strings as JSON would, and `showCopy` for the per-row copy actions.
  • `redact` takes any `(context) => boolean` over `{ path, key, kind, value }`, and `redactedLabel` sets what the masked row shows. There is no default redactor — a component cannot know which of your fields is a secret, and one that guessed would hide data the reader came to read.
  • `evaluateGetters` switches accessors from listed-unread to read-in-a-try/catch; a getter that throws shows its message rather than taking the panel down. Only own accessors are listed — a `get` in a class body lives on the prototype, and the prototype is named rather than walked.
  • `nodeClick` reports `{ path, key, value }`; `copied` reports `{ path, what, text, redacted }`. On a masked row both carry the mask, not the secret.

Best practices

  • Do: Leave `expandDepth` low (1–2) for large payloads — a fully expanded response is a DOM node per value.
  • Do: Give `rootLabel` the name of the thing (`response`, `payload`), since it is what the copied paths start from.
  • Do: Pass `redact` on any viewer pointed at a request, a session or a config. `secretKeyRedactor` is one import and covers the usual key names.
  • Do: Lower `maxRows` on a panel inside a modal or a drawer. It is the only limit that survives nesting.
  • Tip: Copy path yields a real accessor (`$.items[3]["content-type"]`) that pastes straight into a test or a console.
  • Tip: `copied` carries `redacted`, so an audit log can record that a masked field was copied without recording what was in it.
  • Don't: Don't pre-stringify the value — passing the object is the whole point; a string renders as a string.
  • Don't: Don't rely on `maxChildren` alone to keep the tree small. It is a width limit and cannot see depth.
  • Don't: Don't turn on `evaluateGetters` for an object you did not construct. A getter can hit the network, mutate state or throw.
  • Don't: Don't deep-clone or filter the value yourself to hide a secret — `redact` keeps the object intact and the mask out of the clipboard, which a clone cannot do for the paths you forget.

Accessibility

  • A real `role="tree"` of `role="treeitem"` rows with `aria-level` and `aria-expanded` — flattened rather than nested, so depth is announced without a DOM nest that cannot be traversed.
  • One tab stop for the whole tree; Up/Down move, Right opens then descends, Left closes then climbs to the parent, Home/End jump to the ends and `*` expands everything.
  • Type is carried by more than colour — every value renders its own text (`null`, `undefined`, `NaN`, `Map(2)`, `[Circular]`), so nothing depends on being able to tell green from blue.
  • A masked row says `[redacted]` in words. There is no icon-only or tint-only version: "this field was hidden from you" is not conveyable in colour, and the label is what a screen reader and a translator both need.
  • An unread accessor reads as `(getter — not read)` and one that threw reads as its message, so neither looks like a value that happens to be missing.
  • Skipped own properties are counted out loud under their container ("+ 1 non-enumerable, 1 symbol-keyed own properties not shown") rather than leaving a container whose count is a claim the tree cannot support.
  • The truncation, ceiling and omission notices are `<p>` elements rather than tree items, so they do not appear in the node count assistive technology reads out.
  • A masked row is not expandable, so the keyboard cannot open a subtree that has nothing behind it.

Feedback on JSON Viewer

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.