AISeptember 4, 2026 · 8 min

Getting reliable structured JSON out of LLMs with FastAPI and Pydantic

The moment you want to do something with an AI response — store it, branch on it, feed it to another service — you need structure, not prose. Regexing JSON out of a chatty reply is where AI features break in production. The fix is to make structured output a contract the model must satisfy, then validate it. Here's the reliable pattern.

Stop parsing prose

Asking 'please reply in JSON' and hoping is the fragile path — models add prose, wrap it in markdown fences, or drift the shape. Every modern provider supports structured outputs / function calling, where you supply a JSON schema and the model is constrained to produce conforming output. You define the shape once; the model fills it.

# FastAPI + Pydantic: the schema IS the contract
class Extraction(BaseModel):
    intent: Literal['refund', 'question', 'complaint']
    urgency: int = Field(ge=1, le=5)
    summary: str

resp = client.responses.parse(
    model='...', input=user_msg, text_format=Extraction)
result: Extraction = resp.output_parsed   # already validated

Pydantic isn't just a type hint here — it's the runtime gate. If the model returns urgency 7, validation rejects it before it touches your logic. The same idea applies in TypeScript with a Zod schema: define it once, validate the model's output against it, and treat a validation failure as a retriable error.

Design the schema for the model, not just for you

  • Use enums/Literals for closed sets — 'intent' as an enum is far more reliable than a free string.
  • Add field descriptions; they act as inline instructions the model reads.
  • Keep it flat and shallow where you can — deeply nested schemas raise the error rate.
  • Constrain ranges and lengths so invalid values fail validation instead of leaking downstream.

A validated schema turns the LLM from an unpredictable text generator into a typed function you can compose with. That's what makes it safe to put in the middle of a pipeline.

Handle the failures you'll still get

Structured output raises reliability sharply but not to 100%. Wrap the call: on a validation error, retry once with the error fed back ('your last output failed validation because urgency must be 1–5'), and if it still fails, degrade gracefully rather than crashing the request. Log the raw output on failure so you can tighten the schema or prompt.

Key takeaways

  • Use structured outputs / function calling — don't regex JSON out of prose.
  • Make a Pydantic (or Zod) schema the runtime contract; invalid output is rejected, not trusted.
  • Design schemas for the model: enums for closed sets, descriptions as instructions, shallow shapes.
  • Retry once with the validation error fed back, then degrade gracefully.

FAQ

How do I make an LLM always return valid JSON?

Use the provider's structured-output / function-calling mode with an explicit JSON schema, then validate the result with Pydantic (Python) or Zod (TypeScript). The schema constrains generation, and validation rejects anything non-conforming. Add a single retry that feeds the validation error back for the rare miss.

Structured outputs vs function calling — what's the difference?

They're closely related: function calling has the model choose and fill a tool's arguments (great when the model decides whether to act), while structured outputs constrain a direct response to a schema (great when you always want the same shape back). For pure data extraction, structured outputs are the simplest fit.

AA
Ali Asghar

Senior software engineer & technical lead — 6+ years shipping production multi-tenant SaaS, payments and AI integration in Next.js, Node & TypeScript.

Keep reading
Building a context-aware AI chatbot with a vector database (RAG done right)Architecting a production-grade AI SaaS with Next.js and FastAPINode.js vs FastAPI: choosing the right backend for your next product