Skip to content

Research note

Output contracts: structured outputs and tool calling

How JSON Schema, tool use, and typed objects form the first protocol layer between LLM responses and frontend interfaces.

Series · Part 2 of 6

LLM Interface Protocols

A factual map of the protocols, specs, and transport layers teams use to turn LLM responses into structured, trusted product interfaces — from output contracts and streaming to AG-UI, A2UI, and MCP Apps.

  1. 1 The protocol landscape for LLM interfaces
  2. 2 Output contracts: structured outputs and tool calling
  3. 3 Streaming protocols: from tokens to UI messages
  4. 4 AG-UI explained for frontend engineers
  5. 5 A2UI explained for frontend engineers
  6. 6 MCP Apps explained for frontend engineers

Before you pick AG-UI, A2UI, or MCP Apps, you need an answer to a more basic question: what shape is the model allowed to return?

That shape is the output contract. It is the lowest layer in the LLM interface protocol stack. Get it wrong and every layer above it — streaming, events, generative UI — has nothing reliable to render.

Key idea

Structured outputs and tool calling are complementary contracts. Structured outputs constrain what the model says; tool calling constrains what the model asks the system to do. Both are schema-driven interfaces on model behavior.

The problem: valid text is not a valid interface

Models naturally produce prose. Prose is flexible and terrible as a machine-to-UI boundary.

Teams tried JSON in the prompt for years. It worked often enough to ship, failed often enough to hurt: trailing commas, markdown fences, hallucinated fields, partial objects mid-stream. The fix was not “prompt harder.” It was constraining generation and validating at the boundary.

Two mechanisms dominate production systems in 2026:

  1. Structured outputs — the model’s visible response must match a JSON Schema.
  2. Tool / function calling — the model emits a structured call to a named function with typed arguments; your runtime executes it and may render the result.

They overlap in syntax but serve different jobs in the interface stack.

Structured outputs: schema as protocol

OpenAI structured outputs

In August 2024, OpenAI introduced Structured Outputs with response_format: { type: "json_schema", ... } on supported models. The critical guarantee: model output adheres to the supplied JSON Schema — not “usually valid JSON,” but schema-constrained decoding.

That shifts frontend work. You can treat the response as a typed object and map it to components without a repair loop. OpenAI documents two entry points:

Anthropic tool use

Anthropic’s tool use returns structured tool_use blocks with input matching your tool schema. Claude is trained to pick tools and populate arguments reliably. For UI purposes, a tool whose output you render is often indistinguishable from a structured response — the difference is who executes the next step (runtime vs. direct render).

Vercel AI SDK: Zod as the contract language

The Vercel AI SDK exposes generateObject and streamObject with Zod schemas. Zod is not an open wire protocol, but it is a practical contract layer thousands of TypeScript frontends already use:

import { z } from "zod";
import { generateObject } from "ai";

const Comparison = z.object({
  kind: z.literal("comparison"),
  items: z.array(
    z.object({
      name: z.string(),
      pros: z.array(z.string()),
      cons: z.array(z.string()),
      score: z.number().min(0).max(10),
    }),
  ),
  recommendation: z.string(),
});

const { object } = await generateObject({
  model: "anthropic/claude-sonnet-4.5",
  schema: Comparison,
  prompt: "Compare three deployment options for a Vue SPA.",
});

The SDK validates and types the result. Your component catalog keys off object.kind or distinct schemas per surface.

flowchart LR
  P["Product decision"] --> Z["Zod / JSON Schema"]
  Z --> M["Model generation"]
  M --> V["Validation"]
  V --> C["Component render"]

Tool calling: actions as structured output

Tool calling is how models request side effects — query a database, send a draft, update state — without embedding imperative code in the response.

A minimal tool definition (OpenAI-style):

{
  "type": "function",
  "function": {
    "name": "show_recommendations",
    "description": "Present ranked options to the user",
    "parameters": {
      "type": "object",
      "properties": {
        "items": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "title": { "type": "string" },
              "confidence": { "enum": ["low", "medium", "high"] }
            },
            "required": ["title", "confidence"]
          }
        }
      },
      "required": ["items"]
    }
  }
}

The interface pattern:

  1. Model returns tool_call with structured arguments.
  2. Runtime does not blindly execute UI tools — it routes to a renderer or asks for approval.
  3. Frontend maps show_recommendationsRecommendationList component.

This is the bridge between “LLM output” and trust boundaries. Tool schemas name intentions; your action layer decides execution.

MechanismBest forFrontend receives
Structured outputUser-visible typed content (cards, tables, plans)JSON object → component
Tool callingActions, lookups, multi-step agent loopsTool name + args → handler / UI
Both combinedAgents that explain and actText stream + parallel tool events

How this connects to generative UI protocols

Output contracts are necessary but not sufficient for cross-app generative UI.

Your app’s own Zod types are a fourth contract — perfectly fine when you control both ends.

The structured outputs foundation post made the product case: do not flatten typed objects back into paragraphs. At the protocol level, the same rule applies: pick a schema before you pick a streaming library.

Design rules that survive vendor changes

  1. Design from the user decision backward. Schema fields should map to UI affordances and actions, not to whatever the model finds easy to emit.
  2. Keep schemas small. Large nested schemas are harder for models to fill and harder for components to render mid-stream.
  3. Separate “display” from “execute.” A proposed_email object is display; send_email is execution. Mixing them in one schema blurs trust boundaries.
  4. Version your contracts. Treat breaking schema changes like API migrations.
  5. Prefer enums over free strings for routing (kind, confidence, status) so component maps stay exhaustive.

Product implication

Structured outputs turned reliable JSON from a prompting exercise into a platform feature. That lowers the cost of typed interfaces — and raises the cost of ignoring them.

If your copilot still renders every result as markdown inside a bubble, you are not “avoiding complexity.” You are paying for structured generation on the backend and throwing away the interface value on the frontend.

Tool calling, used well, is how you keep actions legible: named operations with typed arguments, previewable in UI, gateable with approval flows.

What to watch next

Output contracts define what arrives. The next layer defines how it arrives while still generating: Streaming protocols: from tokens to UI messages.

References:

Previous
Streaming protocols: from tokens to UI messages
Next
The protocol landscape for LLM interfaces