A tool result starts life as data — a row, a record, a number the server already had — and then, in the prose-only shape, gets flattened into a sentence. Everything downstream pays for that flattening: the host re-parses text to recover fields it should have been handed, and the model is drafted into transcribing values from one call into the next. Structured tool output stops the loss at the source. A tool declares an outputSchema describing what a successful call returns, and returns a structuredContent object conforming to it, alongside a readable rendering of the same data. This piece covers both fields, why both channels ship together, validation and contract violations, backward compatibility, schema evolution, and when structure is not worth declaring.
Prose results make everybody guess
A tool result is, at its simplest, an array of content blocks — usually one block of text. That is enough when the only consumer is a language model reading an answer in a chat window. It stops being enough the moment anything deterministic wants that answer. A host that needs to draw a chart, cache a row, or compare a value against a threshold has to recover fields out of a sentence, and sentences vary: today Temperature: 27.4C, tomorrow it is currently 27.4 degrees and humid. Regexes accumulate; none of them are testable against a phrasing that has not happened yet.
The model gets drafted into the same job. It reads the prose, extracts a number, and retypes it into the next call’s arguments — tokens spent on clerical work, plus a transcription step that can silently go wrong. Both sides are reconstructing a structure the server already had and discarded the moment it formatted the string. Structured output exists to stop the server discarding it.
outputSchema — declaring the shape a tool returns
A tool definition already carries an inputSchema: a JSON Schema describing the arguments a caller may send. Current revisions of the protocol let a tool also carry an outputSchema — conventionally an object type — describing the fields a successful result will contain. It is the mirror image of the input contract, published the same way and in the same place: in the tool listing, at discovery time, before any call happens.
Publishing the shape early is what makes it useful. A client can compile a validator once instead of per call. A host can decide in advance which renderer or route fits this tool. And a model told what a call returns can judge whether the tool serves its goal at all, rather than calling it to find out. Declaring the schema is also a promise: the server commits that what it sends back will conform to it.
structuredContent — the typed payload
The other half of the contract lives in the result. Alongside the familiar content array, a tool result may carry a structuredContent field holding exactly one JSON object that matches the declared outputSchema. It is a single object rather than a list of blocks precisely because it is not presentational: it is the deserialized return value, the thing a program will index into.
The distinction is worth holding onto, because the two fields answer different questions. content is a channel of things to display or read — text, images, embedded resources, ordered for legibility. structuredContent is a value. A field named total there is the total, at a known type, at a known path, whatever the tool chose to say in its summary. Once a result carries a value channel, the host stops doing natural-language processing on its own tool output — which was never a reasonable thing to ask of it.
Why both channels ship together
The obvious simplification — send the structured object and drop the prose — is the one the design deliberately refuses. A tool that returns structuredContent is expected to also include a text representation of the same data in content, typically the JSON serialized into a text block.
There are two consumers and they want different things. The model reasons over language; handing it a bare object with no readable rendering makes it worse at exactly the summarizing and explaining you invoked it for. The application wants a typed value and no parsing at all. Sending both costs a modest duplication and serves each consumer in its native currency. One rule keeps the arrangement honest: the two views must agree. structuredContent is authoritative for programs and content is a rendering of it, not a second, subtly different answer travelling in the same envelope.
Validation, and what happens when a tool breaks its own contract
Validate on both ends. A server should check its own payload against its declared outputSchema before returning, because a handler is ordinary code and ordinary code drifts from its documentation. A client should check on receipt, because a contract nobody enforces is only a comment. Wiring that same check into a test suite catches most drift before a user ever sees it.
When a payload fails, treat it as a broken tool rather than a confused model. A temperature arriving as the string "27.4C" where the schema promised a number is a server defect, and catching it at the boundary is enormously better than letting the value travel into a gauge, a comparison, or the next call’s arguments, where it fails somewhere unrelated and unrecognizable. What the client does next — reject outright, or fall back to the readable content with the structure marked untrusted — is a policy choice, and a separate matter from a tool that ran correctly and legitimately reported failure.
Backward compatibility with clients that ignore structure
Not every client understands structured output, and a server generally cannot know which ones do. That is precisely why the mirrored text block is not optional politeness. An older client reading only content receives a serialized JSON string and displays it: it loses the typing, but it does not lose the answer. Nothing breaks, no negotiation flag is required, and a server can start emitting structure without waiting for the ecosystem to catch up.
The obligation runs the other way too. A client that does understand structured output must not assume it is present. A tool that declares no outputSchema, an older server, or an error result will send content alone, and the client’s fallback has to be to use the text. Treat structured output as an enhancement layered on a channel that still works without it, and its absence becomes an ordinary case rather than a crash.
Evolving a schema without breaking callers
A published schema is an interface, and interfaces change. The safe direction is additive: new fields arrive optional, existing fields keep their names and types, and clients ignore properties they do not recognise. A server that adds an optional station_id to its forecast result breaks nobody. A server that renames temperature to temp_c, narrows a type, or promotes an optional field to required breaks every caller that had been reading the old shape.
Two habits keep this manageable. Resist additionalProperties: false on results unless you truly need the closed world — a strict schema turns your own additive change into a validation failure at every client that cached the older version. And when a genuinely breaking change is unavoidable, ship it as a new tool name beside the old one, so callers migrate on their own schedule instead of discovering the change through failures.
Typed results compose; prose does not
The payoff shows up in chains. When a forecast tool declares a numeric temperature and an irrigation tool declares an input expecting a number, a host can wire the one into the other, confirm the types line up before invoking, and skip the model for that hop entirely. The model is then spent on the decisions that need judgment rather than on copying values between tools, which is cheaper and harder to get wrong.
Even where the model stays in the loop, it now plans against a known shape. Knowing before the call that a result will carry conditions and humidity lets it commit to a plan rather than call speculatively and improvise over whatever text comes back. That is the real reframing: a call with a declared input schema, a declared output schema, and validation on the wire is a typed function call that happens to cross the model boundary.
When structure is not worth it
Structure earns its keep when a result has stable fields that something other than the model will read. It earns nothing when it does not. A tool that returns a document, an explanation, a generated paragraph, or a diff has no meaningful field decomposition; wrapping it as {"text": "..."} adds a schema, a validator, and a duplicated payload while buying nothing over the text block that was already there.
Two more cases argue against declaring one. A result whose shape genuinely varies per call is better left undeclared than described by a schema so permissive it validates anything — a contract that forbids nothing is misleading documentation. And a very large result, thousands of rows, probably should not be inlined at all; a resource the host can fetch on demand serves better. Declare an outputSchema when a program will read a field by name. Otherwise, send good text and stop there.