AI agents
Key points

Key points

  1. 1Field order inside a JSON object decides how soon a streaming consumer can act on the data it has already received.
  2. 2A shallow root object with scalars first and nested structures later keeps the useful payload near the top of the stream.
  3. 3A top-level array is the hardest shape to render incrementally because nothing is addressable until elements reach a valid boundary.
  4. 4Wrapping arrays in an object with a count and an items array converts an unusable stream prefix into a renderable one.

Why JSON.parse gives you nothing until the last byte

JSON.parse is all-or-nothing. It needs a balanced document before it returns a value, so a token stream that is still open is not a state the standard parser models, it is an input error 12. Any consumer that buffers until the closing brace pays the full generation latency before showing anything.

That latency is real. A multi-kilobyte structured response from a frontier model takes seconds to emit in full, and industry write-ups cited in that same analysis put the perceived-latency improvement from progressive rendering at roughly 60 to 70 percent, not because generation gets faster but because motion replaces waiting 12. The schema you write determines how much of that window you can fill with useful pixels.

The obvious workaround is worse than the wait: call JSON.parse or a repair library on every chunk. That re-parses the whole prefix per token, which is quadratic. The same write-up records the measured difference: a naive 12 KB parse took 16.7 seconds with final chunks at 19 to 20 milliseconds each, while a stateful incremental parser ran the same workload in 43 milliseconds, a 388x gap, and the naive version became unusable around the 5 KB mark 12. So the baseline already assumes you have an incremental parser. The schema question is what that parser can hand you early.

Incremental parsers exist and are the foundation. jsonriver yields a series of partial values designed to plug into UI = f(state) rendering 11. Other libraries follow the same idea, including jsonchunk 15, chunkjson 20 and best-effort-json-parser, whose README shows a truncated input like [1, 2, {"a": "apple returning [1, 2, { a: 'apple' }] 14. But none of them can invent structure your schema did not contain. A parser gives you a floor; the schema sets the ceiling.

Pattern 1: put the fields a consumer needs first

JSON objects are ordered, and most streaming parsers surface keys in document order. That means the first key the model emits is the first key your renderer can bind to. Put the identity and decision fields first, and the bulk payload last.

{
  "type": "object",
  "properties": {
    "status":      { "type": "string", "enum": ["ok", "partial", "failed"] },
    "confidence":  { "type": "number" },
    "title":       { "type": "string" },
    "summary":     { "type": "string" },
    "risks":       { "type": "array", "items": { "type": "string" } },
    "citations":   { "type": "array", "items": { "type": "object" } }
  },
  "required": ["status", "confidence", "title", "summary", "risks", "citations"]
}

A schema-aware streaming layer can validate a field as soon as its type is decided, so a wrong-typed confidence is caught while the rest of the document is still generating and a retry is still cheap 12. Field order converts that from a nice sentence into a policy: status and confidence arrive in the first few dozen tokens, so a UI can paint a badge and a score before the summary exists.

There is a hard constraint to respect while you reorder: strict schema modes on major providers require every property to be listed in required and usually reject optional keys, which is why community threads keep recurring on what strict actually enforces compared with function-calling required 3. Order for progressive rendering, but do not reach for optional to do it. If a field may be absent, model it as a nullable value or move it into a later object.

The decision rule: if a field changes what the user sees first (a verdict, a score, a classification), it goes in the first third of the property list. If it is prose the user reads after the verdict, it goes after. Raw evidence goes last.

Pattern 2: keep the root shallow

Depth is the second cost. A value nested three levels down is not usable until all three enclosing objects are open, and its aliases, inner arrays and metadata each add tokens in front of it. Move nested structures into later sibling fields and the useful part of the document arrives sooner.

Root shapeFirst field usable atPartial parse costRender complexity
Verdict nested under meta.result.decisionAfter meta and result are openHigher, more open containers to trackNeed path guards at every level
Verdict as root key decisionAfter the first keyLowestBind directly
Payload nested under payload.records[]After payload opens, per elementModeratePer-record widgets
Records as late root key recordsAfter earlier scalar fields closeModerateSame, but verdict already on screen
Top-level array [{...}]Never before a complete documentHighestHardest, see below

The pattern that works is a flat root with a small number of scalar fields and two or three late object or array fields:

{
  "decision": "approve",
  "confidence": 0.86,
  "reason": "All risk clauses present.",
  "record_count": 240,
  "records": [ { "id": "r-1", "amount": 12.5 } ],
  "audit": { "model_calls": 2, "tokens_used": 5120 }
}

Two practical rules fall out of this. First, never make the root an object whose only child is another object; {"data": {"data": {...}}} costs tokens and delays every field. Second, put diagnostics, debug metadata and token accounting in a final field. Nobody renders them during generation, and putting them last means they never block the useful payload.

If your stack already generates tokens under constrained decoding, field order is respected by the grammar, so the ordering trick is reliable rather than advisory. Grammar-based decoding compiles the schema into a state machine or grammar that filters invalid tokens, and the survey of that literature describes libraries that are orders of magnitude faster than earlier attempts 1. The practical consequence is that with constrained decoding you can treat schema order as a contract, and without it you are persuading the model with prompts and validating afterwards.

Pattern 3: never return a top-level array

This is the shape that defeats incremental rendering. For [{"id": 1}, {"id": 2}, {"id": 3}], the opening bracket tells your parser that an array started, but there is no key to bind to and no named value to display. You get element values only as each one closes, and you have no place to put document-level metadata such as a total count. Stack Overflow's top-level array thread shows the related downstream symptom: a consumer written for an object receives a bare list and breaks on the first property access 19.

Wrap it:

{
  "type": "object",
  "properties": {
    "total_count": { "type": "integer" },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id":     { "type": "string" },
          "title":  { "type": "string" },
          "score":  { "type": "number" }
        },
        "required": ["id", "title", "score"]
      }
    }
  },
  "required": ["total_count", "items"]
}

The count arrives first, so a UI can show "0 of 40 loaded" before the first item finishes. The items array streams element by element, and each complete element is a named child of a named array, which makes it straightforward to append a row as it closes. This is exactly the pattern streaming tooling converges on: the TanStack AI release exposes partial as DeepPartial<T> and final as T | null from a single outputSchema, so the client gets typed incremental values instead of a bare array it has to chase 17.

Be explicit about count honesty. There are two viable contracts. Either total_count is estimated and the client accepts it growing, or it is exact and the model must know the total before it emits items, which pushes you toward pre-computing the list server-side and streaming it. Pick one and document it in the schema description. A client that renders a progress bar against a count that later falls is worse than no progress bar. If you cannot guarantee exactness, name the field estimated_count and render it as approximate.

Client rules for telling complete objects from partial ones

A schema change is only half the work. The consumer has to know which values are finished. Track completion per value, not per document, and gate any side effect (database write, tool call, payment) behind completion.

type Completion = { path: string; done: boolean };

// Overflow copies of thrown exceptions; the exception object itself is partial.
function shouldCommit(path: string, complete: Set<string>): boolean {
  return complete.has(path);
}

The practical rules:

  1. Untrusted input. The model's output is external input, so validate it at the boundary and make truncation a loud, handled case rather than a silent parse failure that leaks malformed data three steps downstream 7.
  2. Render on arrival, commit on closure. Update the DOM from partial objects, but only persist objects whose closing brace or bracket has been seen.
  3. Escape hatch on truncation. If the connection drops mid-document, decide per field type: a chat-style prose field can show a partial string, a tool argument cannot, because executing a half-typed argument is worse than waiting one chunk 12.
  4. Mark the terminal state. Your UI needs a distinct final event, otherwise "still arriving" and "finished with no more data" look identical. TanStack's design adds a single tagged completion event carrying the validated object for exactly this reason 17.

One more caution: not every provider streams constrained JSON deltas even when it always returns valid structured output at the end. A Claude Code feature request documents that with --json-schema the CLI streamed natural language deltas while the structured object appeared only in the final result, with the workaround being prompt-level JSON plus a partial parser, at the cost of no constrained decoding 18. So verify, for your provider and mode, whether partial JSON deltas actually arrive. If they do not, schema ordering buys you nothing on that path and you should compare providers or modes rather than rewrite your schema for a stream you never see. Quanta Inference is useful here because you can send the same schema to text, image and audio models through one OpenAI-compatible API key and observe which ones stream structured deltas, rather than maintaining a different client per vendor: https://quantaintelligence.ai/inference.

Worked example: the same data, three shapes

Take a task that returns a 40-item list of supplier records at roughly 300 bytes per record, about 12 KB total, with a decision verdict at the front.

Naive shape (top-level array): the first record can be rendered only after its closing brace, and the decision field only after roughly the whole document if it is not first. Time to first useful pixel is close to full generation time.

Ordered root object, records inline with the verdict first: verdict and confidence render in the first few dozen tokens, the count renders next, records append one by one. Time to first useful pixel is near zero from the user's view.

Deeply nested root: verdict sits under meta.check.result.decision, so three objects must open before the verdict is addressable, and every field below adds nesting tokens to the prefix. Time to first pixel moves later and the parser tracks more open containers.

The same 12 KB response needs the order of magnitude estimate the quadratic analysis produced to matter at all, which is why fixing the shape and fixing the parser are separate jobs that both need doing 12.

A quick review checklist before you ship a streaming schema:

CheckFail symptomFix
Verdict or score in the first two propertiesSpinner until the endReorder properties
Root is an object, not an arrayNo named values earlyWrap with total_count and items
Nesting under root no deeper than needed for the first screenLate first renderFlatten, move structures to later siblings
Every property listed in required, absent values nullableStrict mode rejectionModel absence explicitly 3
Diagnostics lastMetadata delays payloadMove to a final audit field
Completion states distinguished in the clientPartial data committedPer-value completion tracking

Choosing between structured outputs and tools for streaming

Two mechanisms produce JSON, and they stream differently. Structured outputs constrain generation to a grammar so the response matches the schema, while function calling asks the model to select a tool and fill arguments over a multi-turn flow 2. For a UI that renders a result, structured outputs with a single response is the simpler stream. For an agent that must act, tool argument streaming has the same ordering problem, and the same fix: arguments a tool handler needs to start work go first, cosmetic ones go last.

One caution on replacing function calling with structured output wholesale: a write-up arguing for that substitution notes that the classic problem it solves is malformed JSON from weaker or local models, and that major vendors already addressed it with strict mode 5. Schema order is worth doing regardless, but do not move off tool calling purely for JSON reliability if your provider already guarantees the shape.

Making it a contract

Three changes, in this order. First, audit every streamed schema for a top-level array and wrap it. That is the highest-value fix per hour spent. Second, reorder properties so the decision fields come first and diagnostics last. Third, add client-side completion tracking and a distinct final event, and test with a deliberately truncated stream, not just a healthy one, because the truncation path is the one that silently corrupts data 9. If you would rather have a specialist review the schemas and the client rules end to end, a consulting call through the Quanta platform can put someone on the specific stream you are shipping: https://quantaintelligence.ai/

FAQ

FAQ

Does field order in JSON Schema actually affect streaming, or do parsers ignore it?

Most streaming parsers surface keys in the order they appear in the token stream, so the first property in the schema is the first value a renderer can bind to. Chapter-and-verse validation does not require order, but progressive rendering depends on it, which is why the ordering trick belongs in the schema rather than a prompt comment.

Why is a top-level array worse than an object containing an array?

A bare array gives your consumer no key to bind to and no place for document-level metadata, so nothing is addressable until elements complete. Wrapping it in an object with total_count and items gives you a named array, a count that arrives first and a stable place to hang status.

Can I keep optional fields and still stream well?

Strict schema modes on major providers generally require every property in required, so optional keys are often rejected outright 3. Model absence as a nullable value and keep the property required, then order the nullable discriminator early so the client knows immediately whether to render the detail view.

How do I know an object is complete and safe to persist?

Track completion per value in the parser, not per document. Render from partial values freely, but gate writes, tool calls and payments on a closing brace or bracket for that specific path, and treat the model output as untrusted input that must be validated at the boundary 7.

What if my provider never sends partial JSON deltas?

Verify before you redesign. Some modes stream prose deltas and deliver the structured object only in the final result 18. If that is your path, schema ordering will not help on that provider; either switch to a mode that streams constrained deltas or accept a single complete render and optimize elsewhere.

Sources

Sources

  1. Streaming JSON Parsers: The Gap Between Tokens and ...
  2. JSON River, Parse JSON incrementally as it streams in
  3. jbingen/jsonchunk: Parse partial JSON as it streams in. ...
  4. danfry1/chunkjson: Streaming JSON for Web ...
  5. best-effort-json-parser
  6. Difference between Structured Outputs and function calling required
  7. Beyond Free-Form Text: How Constrained Decoding is ... - Medium
  8. paring json with top level array - python
  9. Stop Waiting on JSON: Stream Structured Output with One ...
  10. Truncated LLM responses breaking JSON.parse is one ...
  11. Feature Request Stream partial JSON tokens when using
  12. Structured Outputs vs. Function Calling: Which Should Your Agent ...
  13. Structured Output as a Full Replacement for Function Calling
  14. Why Most AI Agents Fail at Structured Output and How to ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles