kmail.at
← learning

gradio · difficulty ◆◆

gradio JSON — print a whole model payload as a foldable tree

gr.JSON is where your demo stops hiding the answer: return the dict you already built and let visitors fold it open themselves.

Your function returns fourteen nested keys and the demo shows the word 'dict'. gr.JSON paints the whole thing as a tree with line numbers, a copy button and collapsible nodes — the same payload, minus the guesswork.

2026-10-14 · 5 min read

$ gr.JSON()

What it does

gr.JSON takes a dict, a list or a valid JSON string and renders it as an interactive tree: line numbers down the left edge, a fold toggle on every object node, keys highlighted in one colour and punctuation in another. Nothing is stringified on the way out — your object goes through orjson and the frontend pretty-prints it, which is why a 400-token API response comes out readable without a single json.dumps in your code. A Copy button sits in the toolbar by default; nodes at depth 3 and below start folded so a huge payload stays a glanceable one. As an input it is legal (preprocess hands the dict straight to your function) but in 6.18.0 it renders no editor at all — data arrives from Python or from the API, never from the keyboard.

Why it matters

A Label throws away everything except argmax, and a Textbox turns your payload into an unreadable repr that loses structure the moment it hits the screen. gr.JSON is the component you leave in the corner while you are still writing the app — token counts, per-source match scores, which branch of the code actually ran, the request id you need for support. HuggingFace's own Blocks demos use it for exactly that: the docstring names zip_to_json and blocks_xray, where a live app's config is dumped into the UI so you can see what you built. Same reason people log the payload; the difference is the visitor never has to open a terminal.

Example

$ import gradio as gr

def inspect_claim(claim: str):
    return {
        "claim": claim,
        "verdict": "supported",
        "confidence": 0.87,
        "evidence": [
            {"source": "report_p12", "match": 0.93},
            {"source": "news_2026_04", "match": 0.61},
        ],
        "model": {"name": "claim-checker-v3", "revision": "a91f2c"},
    }

demo = gr.Interface(
    fn=inspect_claim,
    inputs=gr.Textbox(label="claim", lines=2),
    outputs=gr.JSON(label="raw response"),
    title="Claim inspector",
)
demo.launch()
* Running on local URL:  http://127.0.0.1:7931
* To create a public link, set `share=True` in `launch()`.

curl -X POST http://127.0.0.1:7931/gradio_api/call/inspect_claim \
     -H 'Content-Type: application/json' \
     -d '{"data": ["CO2 rose 12% since 2019"]}'
  -> {"event_id": "f0459ad858d14ee9bfdd45b0c5de58b0"}

event: complete
data: [{"claim": "CO2 rose 12% since 2019", "verdict": "supported", "confidence": 0.87, "evidence": [{"source": "report_p12", "match": 0.93}, {"source": "news_2026_04", "match": 0.61}], "model": {"name": "claim-checker-v3", "revision": "a91f2c"}}]

Rendered UI: a two-line Textbox labelled "claim", the standard Interface Submit button underneath, and one gr.JSON card labelled "raw response" holding the five-key tree — evidence and model fold open as nested objects, Copy sits in the card's toolbar.

Measured on gradio 6.18.0 / Python 3.12: launched with launch(prevent_thread_lock=True), then the same function was driven through the HTTP API so both the startup banner and the served payload are real output.

$ import gradio as gr

PAYLOAD = {
    "model": "claim-checker-v3",
    "usage": {"prompt_tokens": 412, "completion_tokens": 88},
    "flags": ["json_mode", "temperature=0"],
}

with gr.Blocks(title="Payload inspector") as demo:
    gr.Markdown("## Raw payload from the model gateway")
    with gr.Row():
        model = gr.Dropdown(["claim-checker-v3", "reranker-lite"],
                            value="claim-checker-v3", label="model")
        run = gr.Button("Fetch payload", variant="primary")
    payload = gr.JSON(label="gateway response", open=False,
                      show_indices=True, height=260)
    run.click(lambda m: {**PAYLOAD, "model": m}, model, payload)

demo.launch()
* Running on local URL:  http://127.0.0.1:7932

After clicking "Fetch payload" (DOM read out of headless Chromium):
  [data-testid="json"]  -> div.json-holder  style="max-height: calc(100% - 26px)"
  toolbar aria-labels   -> ["Copy", "Collapse", "Collapse", "Collapse"]
  scrollHeight 247px vs clientHeight 232px  -> the card scrolls, the page never grows
  show_indices=True prints array positions as quoted keys:
    "flags": [ "0": "json_mode", "1": "temperature=0" ]

Rendered UI: Dropdown "model" + Button "Fetch payload" in a Row, then a 260px-high JSON card in which model / usage / flags all render expanded (depth 1-2) and deeper nodes would fold at depth 3.

The 26px in that style is the toolbar height: height=260 clamps the tree, not the card. Same run measured with a 5-level payload: with open=False the depth-3 node folds into Object(2), with open=True "c" -> {"d": "deep value"} is printed instead.

$ import gradio as gr
import numpy as np
import datetime

j = gr.JSON()

j.postprocess({"a": 1})                                   # dict as-is
j.postprocess([1, 2, 3])                                  # list as-is
j.postprocess('{"a": 1}')                                 # JSON string gets parsed
j.postprocess({"x": np.array([1, 2, 3]), "y": np.float32(0.5)})
j.postprocess({"ts": datetime.datetime(2026, 10, 14, 9, 30)})
j.postprocess(None)
j.postprocess(42)
j.preprocess({"a": 1})
postprocess({"a": 1})     -> JsonData(root={'a': 1})
postprocess([1, 2, 3])    -> JsonData(root=[1, 2, 3])
postprocess('{"a": 1}')   -> JsonData(root={'a': 1})
postprocess(np payload)   -> JsonData(root={'x': [1, 2, 3], 'y': 0.5})
postprocess(datetime)     -> JsonData(root={'ts': '2026-10-14 09:30:00'})
postprocess(None)         -> None
postprocess(42)           -> UserWarning: JSON component received unexpected type <class 'int'>.
                             Expected a string (including a valid JSON string), dict, list, or Callable.
                             ... then JsonData(root=42)
preprocess({"a": 1})      -> {'a': 1}
postprocess("plain text")  -> Error: 'Invalid JSON string: unexpected character: line 1 column 1 (char 0)'

All eight lines are the real return values from gr.JSON() in gradio 6.18.0. Note the datetime: 09:30:00, not 09:30:00T — PASSTHROUGH_DATETIME sends it to str(), and it only stays a string because it is not valid JSON.

$ import gradio as gr

PAYLOAD = {"model": "claim-checker-v3",
           "usage": {"prompt_tokens": 412, "completion_tokens": 88}}

def summarise(p):
    if not isinstance(p, dict):
        return f"got {type(p).__name__}"
    return ", ".join(sorted(p.keys())) or "(empty)"

with gr.Blocks() as demo:
    load = gr.Button("load payload", variant="primary")
    toolbar_btn = gr.Button("Summarise payload")
    payload = gr.JSON(label="gateway response", buttons=["copy", toolbar_btn])
    summary = gr.Textbox(label="summary")
    as_input = gr.JSON(label="payload you can edit", value={"a": 1, "b": [1, 2]})
    echo = gr.Textbox(label="echo")
    send = gr.Button("Send edited payload")

    load.click(lambda: PAYLOAD, None, payload)
    toolbar_btn.click(summarise, payload, summary)
    send.click(summarise, as_input, echo)

demo.launch()
Toolbar DOM inside [data-testid="json"]: ["Copy", "Summarise payload"]
Clicking "Summarise payload" in the toolbar -> summary Textbox = "model, usage"

API path, because there is no editor in the browser:
  POST /gradio_api/call/summarise  {"data": [{"user": "nadia", "roles": ["admin", "auditor"]}]}
  event: complete
  data: ["roles, user"]

Playwright locator for `textarea, [contenteditable="true"]` inside the JSON component timed out after 30s — in gradio 6.18.0 a gr.JSON wired as an input still renders no editable field.

The custom gr.Button() you pass in buttons= shows up in the component's own toolbar and fires the .click() handler registered on that same button object — verified by clicking it in headless Chromium, not by reading the docs.

Common flags

value
A dict, list or valid JSON string; can also be a function, in which case it is re-called on every app load (pair it with inputs=/every= for a self-refreshing dashboard panel).
open
False by default: nodes from depth 3 down are folded. open=True expands everything — a 5-level payload prints to the leaf.
show_indices
Prints array positions as quoted keys: "flags": [ "0": "json_mode", "1": "temperature=0" ]. Off by default because it doubles the key noise.
height
Number (px) or CSS string. height=260 gives a 260px box whose style is max-height: calc(100% - 26px) — the 26px is the toolbar.
max_height
Defaults to 500 px and renders as max-height: 500px on the block; height=None means auto-fit but max_height still caps it.
buttons
Toolbar row. "copy" is included by default; extra gr.Button() instances appear next to it and trigger their own .click() handlers.
show_label / container
show_label=False and container=False strip the label and the card padding when the tree sits inside your own layout — the tree already looks like a code block.

History

Shipped in gradio 1.1.0 (2020-08-10)

I pulled the wheels: gradio 1.0.0 of 2020-07-09 has no JSON class in gradio/outputs.py, and 1.1.0 of 2020-08-10 has one — the same release that added HighlightedText, Audio, HTML, File and Dataframe. Its entire signature was JSON(label=None); today's is 20 parameters (21 counting self), 11 of them already there in 4.0.0, with open and show_indices arriving in 4.40.0 and buttons in 6.0.0.

Design note: the string branch flipped

1.1.0's postprocess ran json.dumps(y) whenever it was handed a string, so passing the JSON text '{"a": 1}' printed a quoted string, not a tree. Modern versions do the opposite — orjson.loads(value), or raise Error('Invalid JSON string: ...') if it does not parse. I checked the same method in the 4.40.0 and 5.29.1 wheels: identical to 6.18.0. So a JSON string now round-trips into structure, which is why piping an API response body straight into gr.JSON just works.

Fun facts

Pros & cons

pros

  • + Zero formatting work: return the dict you already build — no json.dumps, no indent=2, no string surgery, and the tree keeps the structure.
  • + Survives awkward payloads: NumPy arrays and floats go through orjson with OPT_SERIALIZE_NUMPY, so an ndarray inside your response does not blow up the output.
  • + Built for reading: line numbers, depth-3 folding, a Copy button in the toolbar by default, and a custom gr.Button() in buttons= can run your own handler on the same payload.

cons

  • − Not interactive in the browser: gradio 6.18.0 renders no textarea or contenteditable even when the component is wired as an input — values arrive from Python or the API only.
  • − Overkill for one number or one short string: a Label, Textbox or Markdown reads better, and the tree chrome (line numbers, fold arrows, braces) drowns a single value.
  • − The string branch is strict — invalid JSON text raises Error instead of displaying, so a function that sometimes returns prose needs a Textbox or a type check in front.

Takeaways

  1. 1Return the dict, not a string: gr.JSON serializes it with orjson and prints the tree — json.dumps is only for a string you want parsed back.
  2. 2Give live JSON outputs a height and open=False (height=260 is a good default). Long payloads then scroll inside the card instead of pushing your layout down, and depth-3 nodes stay folded until someone clicks.
  3. 3Set show_label=False and container=False when the JSON sits inside your own card — the tree is already a styled code block.
  4. 4Test your function's worst return value before wiring gr.JSON: None renders empty, an int warns (UserWarning: unexpected type), and a non-JSON string raises Error('Invalid JSON string') at postprocess time.
  5. 5Add buttons=["copy", gr.Button("Summarise")] and register a .click() on that button object to turn the payload card into a control panel for the response itself.

Related commands