GrJsonViewer
Walks an unknown value as a tree: collapsible nodes, search by key or value, copy a node with its path.
Overview
When to take it
- the answer of someone else’s service is picked apart field by field — a webhook, the answer of a model, the body of an error: a key has to be found, not the whole document read;
- the structure is not known in advance — the shape arrives from the backend and changes, and there is nothing to lay it out into columns by;
- a value has to be taken out pointwise — a node is copied together with its path, not the whole sheet;
- there is a lot of data — collapsing, search and virtualisation hold an answer of thousands of nodes.
When to take something else
| Need | Take |
|---|---|
| Read a text or a JSON in full and copy it at once | GrCodeBlock (the @feugene/granularity-code package) |
| A tree of your own data with selection and checkboxes | GrTree |
| The data is laid out in columns and known in advance | GrDataTable |
| A “property → value” pair | GrDescriptionList |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrJsonViewer
:value="value"
rootLabel="order"
:defaultExpandDepth="1"
searchable
/>Examples 3 each with a live demo and its code
Webhook payloads with readable values
default-expand-depthopens as much as fits in one glance, andcopyablehands a subtree over as JSON text. The viewer does not know what a value means — the app does: theleafslot draws cents as euros, ISO dates as dates and URLs as links, while the copied value stays the raw one.Webhook deliveries · https://api.northwind.example/hooks/billing payload:{4}id:"evt_3Q8x"type:"invoice.paid"created:data:{1}invoice:{6}id:"INV-0418"customer:{3}amount_paid:€1,284.50currency:"eur"hosted_url:https://pay.example.com/i/INV-0418lines:[2]A customer record for a support engineer
searchablefinds a key or a value without reading the whole response, and the support team’s saved lookups — «Cards», «VAT» — drive the same search through the exposedfilter().max-heightkeeps the tree in a window of its own, andcopyreports the path that went to the clipboard.GET /v1/customers/cus_N7200 OK · 86 ms · support viewcustomer:{11}id:"cus_N7"object:"customer"name:"Northwind Traders"email:"[email protected]"tax:{4}address:{4}subscription:{6}payment_methods:[2]metadata:{3}balance:-1250delinquent:falseA model request log within limits
A base64 image and a 1,536-number embedding make a tree unreadable and slow.
max-string-lengthcuts the image to a stub — copying still gives the whole value —max-array-itemsstops the embedding after twenty numbers, andvirtualkeeps only the rows near the viewport in the DOM.Request log · req_7f2cA 22 KB image and 1,536 numbers, shown as much as is useful log:{5}id:"req_7f2c"model:"vision-large-2026-08"latency_ms:2140request:{2}messages:[2]0:{2}role:"system"content:"You read receipts and return the merchant, the date and the total."1:{2}role:"user"content:[2]0:{2}type:"text"text:"Taxi receipt from the Lisbon trip."1:{2}type:"image"source:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42m"…temperature:0response:{3}output:{4}merchant:"Taxi Lisboa"date:"2026-10-16"total:24.6currency:"EUR"embedding:[1536]0:01:0.2054452:0.3746023:0.4775934:0.4962275:0.4272136:0.2827417:0.0883278:-0.1216879:-0.31020810:-0.44393611:-0.49925312:-0.46638713:-0.35114314:-0.17387715:0.03410116:0.23605617:0.39631618:0.48657519:0.490891…:1516moreusage:{2}input_tokens:1288output_tokens:31
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The type size of the tree and the height of the search field and the panel buttons. Unset — from `GrConfigProvider`, otherwise `md`. |
ariaLabel | string | undefined | undefined | The name of the area for a screen reader. A nameless tree is announced simply as "tree". |
virtual | boolean | undefined | false | Keep in the DOM only the window around the viewport. It requires `maxHeight`. |
maxHeight | string | number | undefined | undefined | The height of the viewing area with its own scroll — even without `virtual`. A number is pixels, a string is taken as is. |
rootLabel | string | undefined | undefined | The label of the root. `$` by default — that is how a JSONPath and dev tools address a path. |
defaultExpandDepth | number | undefined | undefined | How many levels are expanded to begin with. |
maxStringLength | number | undefined | undefined | The length of a string value beyond which the display is truncated. Not cosmetics: a request to a model with a picture in base64 is one leaf of hundreds of thousands of characters, and neither collapsing nor virtualisation by row takes it. The copying meanwhile gives away the value in full. |
maxArrayItems | number | undefined | undefined | How many elements of an array to parse before the "N more" placeholder. |
searchable | boolean | undefined | undefined | A panel above the tree: a search field and "Expand all / Collapse all". **On** by default (the default is changed through `GrConfigProvider`). `false` removes the panel entirely: search stays with `filter()`, expanding with `expandAll()`/`collapseAll()` from `defineExpose`. |
copyable | boolean | undefined | undefined | A button that copies the node, in the row. |
valuerequired | unknown | — | The value being shown. `unknown`, because it comes from a database or from someone else’s service. |
Slots
| Slot | Type | Description |
|---|---|---|
leaf | { node: GrJsonNode; } | The value of a leaf: a link, a date, a sum of money — the styling is known by the application. |
Events
| Event | Signature | Description |
|---|---|---|
copy | (event: "copy", payload: { path: string; value: unknown; }): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
filter | (value: string) => void | Filter the tree by key or value — with the same query as in the search field. An empty string removes the filter. |
expandAll | () => void | Expand all branches. |
collapseAll | () => void | Collapse all branches, including the root. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrJsonViewerConfigurableProps, GrJsonNodeKind, GrJsonNode } from '@feugene/granularity/components/GrJsonViewer'Filedist/types/components/GrJsonViewer/config.d.ts
const grJsonViewerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrJsonViewer">;Filedist/types/components/GrJsonViewer/defaults.d.ts
interface GrJsonViewerConfigurableProps {
size: GrComponentSize;
defaultExpandDepth: number;
maxStringLength: number;
maxArrayItems: number;
searchable: boolean;
copyable: boolean;
}Filedist/types/components/GrJsonViewer/jsonToNodes.d.ts
type GrJsonNodeKind = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'type GrJsonNode = {
path: string;
label: string;
kind: GrJsonNodeKind;
preview: string;
value: unknown;
truncated: boolean;
isLeaf: boolean;
children?: GrJsonNode[];
};Filedist/types/components/GrJsonViewer/GrJsonViewer.vue.d.ts
interface GrJsonViewerProps {
value: unknown;
rootLabel?: string;
defaultExpandDepth?: number;
maxStringLength?: number;
maxArrayItems?: number;
searchable?: boolean;
copyable?: boolean;
virtual?: boolean;
maxHeight?: string | number;
ariaLabel?: string;
size?: GrComponentSize;
}interface GrJsonViewerEmits {
(e: 'copy', payload: {
path: string;
value: unknown;
}): void;
}Filedist/types/components/GrJsonViewer/safelist.d.ts
const grJsonViewerSafelist: string[];Filedist/types/components/GrJsonViewer/index.d.ts
type GrJsonViewerInstance = ComponentExposed<typeof GrJsonViewerComponent>;Accessibility
APG pattern
tree (через GrTree)
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The display is truncated, the copying is not
A long string value is truncated in the row (maxStringLength), and a long array is cut off
with a “N more” placeholder (maxArrayItems). That is not cosmetics: a request to a model
with a picture in base64 is one leaf of hundreds of thousands of characters, and neither
collapsing by node nor virtualisation by row takes it, because there is one node there and
one row.
The full value of the node goes into the clipboard together with the path: the truncation belongs to the display, and pasting it back is not possible.
A repeated reference is not a cycle
The [Circular] marker goes only to a real ancestor along the chain, and an object honestly
put into the data twice is drawn twice.
This is the case where walking a tree can strictly more than serialisation: the replacer of
JSON.stringify does not receive the stack of ancestors and is forced to mark any repeated
reference. A walk has the stack, so GrCodeBlock and GrJsonViewer give different — and
each in its own way correct — results on the same data.
The address of a node is readable
The key of a node is its path ($.items[3].name) rather than an ordinal number: it leaves in
the copy event, and the expansion is set by it as well. A key with a dot or a space is
escaped ($["a.b"]), otherwise the address would stop being an address.
The expansion is set by depth, not by a list
defaultExpandDepth expands the first N levels; expandAll() and collapseAll() from
defineExpose switch the whole tree. Both buttons reset the user’s manual expansion —
that is what is expected of “expand everything”.
The search runs over the key and the value
In someone else’s answer people look now for one, now for the other, so the predicate looks both at the name of the key and at the displayed value. The matching nodes are highlighted by the tree as a whole, and the path to them is expanded.
The panel above the tree — the search field and “Expand all / Collapse all” — is on by default.
It can be removed (searchable: false) and filter(query), expandAll() and collapseAll()
called from the outside — when a search bar already exists on the page and a second one would be
redundant.
maxHeight bounds the viewing area with its own scroll even without virtual.
Limits
It does not edit and does not diff: the viewer shows what has arrived. There is no
highlighting of the matched substring inside a string — the tree marks the node as a whole.
The virtualisation switches on only together with maxHeight
(GrTree): without a height there is nothing to compute the window from.
Props in this example