JSON Viewer
FreeExpandable 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.
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 demoMore examples
A webhook payload, opened two levels
<sv-json-viewer [value]="payload()" rootLabel="payload" [expandDepth]="2" size="sm" />
A very large array, capped
<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
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
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
<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
<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
@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
| Name | Type | Default | Description |
|---|---|---|---|
| value | unknown | undefined | The value to show. Anything at all — this is not restricted to parsed JSON. |
| rootLabel | string | 'root' | Name of the root node. |
| expandDepth | number | 1 | Auto-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. |
| maxChildren | number | 100 | Maximum 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. |
| maxRows | number | 2000 | Maximum 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. |
| size | JsonViewerSize | 'md' | Type scale and row density. |
| sortKeys | boolean | false | Sort object keys alphabetically instead of insertion order. |
| quoteStrings | boolean | true | Render strings with quotes and escapes, as JSON would. |
| showCopy | boolean | true | Show the copy-path / copy-value action on the focused row. |
| redact | JsonRedactor | null | null | Masks 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. |
| redactedLabel | string | '[redacted]' | What a masked row shows in place of its value. |
| evaluateGetters | boolean | false | Read 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. |
| ariaLabel | string | 'JSON' | Accessible name for the tree. |
| customClass | string | '' | Extra classes on the root element. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| nodeClick | JsonNodeClick | A row was activated (click or Enter). |
| copied | JsonCopyEvent | Something 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. |
| copyAudited | SvSensitiveCopyEvent | Something 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.