Structured, awaitable access to streaming JSON sources.
Project description
jsontap
Progressively consume structured output from an LLM as it streams.
jsontap lets you await fields and iterate array item as soon as they appear – without waiting for full JSON completion. Overlap model generation with execution: dispatch tool calls earlier, update interfaces sooner, and cut end-to-end latency.
Built on top of ijson, it provides awaitable, path-based access to your JSON payload, letting you write code that feels sequential while still operating on streaming data.
For more details, here's the blog post.
Install
pip install jsontap
Or with uv:
uv add jsontap
The problem with how agents work today
Most agent frameworks handle structured JSON output in one of a few ways:
- Buffer and parse – wait for the full streamed response, then call
json.loads. Simple, but you're leaving tool execution latency sitting idle during generation. - One tool call at a time – prompt the model to return a single tool call, execute it, then go back for the next. Clean, but fully sequential. You lose all parallelism.
- Newline-delimited hacks – instruct the model to output one JSON object per line and split on
\n. Brittle, prompt-dependent, and breaks with any formatting variation.
jsontap eliminates these workarounds with a clean async API built around an iterative JSON parser ijson.
Sequential code, progressive execution.
With jsontap, you write code that reads top-to-bottom, like you're accessing a fully parsed JSON – except each line resolves as the data arrives:
response = jsontap(chat_completion())
reasoning = await response["reasoning"]
print(f"{reasoning}")
async for call in response["calls"]:
tool = await call["tool"]
args = await call["args"]
asyncio.create_task(dispatch(tool, args))
summary = await response["summary"]
print(f"{summary}")
This looks like code you'd write against a fully parsed JSON – but it isn't. Each await and async for resolves as the corresponding part of the JSON arrives in the stream. reasoning unblocks the moment that field is parsed, the calls loop starts iterating before the array is complete, and summary waits only as long as it needs to.
Here's a showcase of the complete example
In practice, this means you can add responsiveness to an agent without restructuring your code. If you already have logic that reads from a parsed JSON, the jsontap version looks almost identical.
Other access patterns
jsontap supports several ways to consume nodes depending on what you need:
# Await a scalar field
intent = await response["intent"]
# Await a nested value
city = await response["user"]["address"]["city"]
# Async-iterate array items as handles (access sub-fields per item)
async for item in response["results"]:
score = await item["score"]
print(score)
# Async-iterate array as fully materialized values
async for result in response["results"].values():
process(result)
# Await the full array at once
results = await response["results"]
Nodes are created lazily and can be subscribed to before their part of the JSON has been parsed. Multiple consumers can await or iterate the same node – each gets the full result.
Limitations
- String values cannot be iterated at the moment. See issue.
- This is not a general-purpose JSON library.
- There's no support for custom field decoders (e.g., to parse dates).
Error handling
| Situation | Behavior |
|---|---|
| Malformed JSON or source exception | All pending awaits and async for loops receive the exception immediately |
| Key not present in parsed JSON | KeyError raised once parsing is complete |
.value accessed before node resolves |
LookupError |
for ... in node before stream finishes |
RuntimeError |
feed() called after finish() |
RuntimeError |
API reference
jsontap(stream) -> AsyncJsonNode
| Source type | Behavior | Returns |
|---|---|---|
AsyncIterable[str] |
Background parsing task starts immediately | AsyncJsonNode |
AsyncJsonNode
| Pattern | Description |
|---|---|
node["key"] |
Get or create a child node |
await node |
Block until value is resolved |
async for item in node |
Stream array item handles as each slot is parsed |
async for value in node.values() |
Stream fully materialized array values |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jsontap-0.6.2.tar.gz.
File metadata
- Download URL: jsontap-0.6.2.tar.gz
- Upload date:
- Size: 9.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c982757cbd6fc47556a8f2ec1dda36c06c3808732edd3231b3e7910a9462b6bb
|
|
| MD5 |
efd427ce69eb6271679c2f8d2728a9ce
|
|
| BLAKE2b-256 |
6055f5220ee1fede584b24063ddbe32596143fba453723820d7a2dea39bf8f19
|
File details
Details for the file jsontap-0.6.2-py3-none-any.whl.
File metadata
- Download URL: jsontap-0.6.2-py3-none-any.whl
- Upload date:
- Size: 10.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ddf25e9efffbf0902633c32c2023e6ef3cea89d1c60af90c221d847f9bd6aeff
|
|
| MD5 |
b4043fa80b0ce8b26e0fbaf9333f3a0d
|
|
| BLAKE2b-256 |
9abe2aba33961bf071da4f90903a561720ae30129ae67817d4ad785a0337cf5e
|