AlgoMaster Logo

Structured Output from LLMs

9 min readUpdated June 22, 2026
Listen to this chapter
Unlock Audio

LLMs return plain text by default. That is useful for chat, but awkward when your application needs data.

For example, if you ask a model to extract product information from a review, it might answer like this:

That is easy for a person to read. It is not ideal for code.

Most applications need a predictable shape: a JSON object, a Python dictionary, or a database row. The product name should be in one field, the price should be a number, and the rating should have a known type.

A common workaround is to parse the model's prose with regex, split on colons, or look for patterns such as dollar signs. That works until the model phrases the answer differently. It also gets messy as soon as you need arrays, nested objects, optional fields, or strict types.

main.py
Loading...

The problem is not that regex is bad. The problem is that free-form model text is not a stable API contract. The wording can change across calls, nested data is hard to recover, and values that look right may still have the wrong type.

Structured output fixes this by making the target shape explicit.

The Structured Output Pipeline

No matter which technique you use, the pipeline looks like this:

You send the model a prompt and a schema. The model returns JSON or tool arguments. Your application parses the result and validates it before using it.

That validation step matters. A schema can help the model produce the right shape, but your application still owns correctness before the data reaches a database, workflow, or user-facing feature.

Approach 1: JSON Mode

The simplest approach is JSON mode. You ask for JSON and tell the API to keep the response valid JSON.

How It Works

Set response_format={"type": "json_object"} in the API call. This helps with JSON syntax. It does not define a full schema, so you still need to tell the model which fields to return and validate the result yourself.

main.py
Loading...

Limitations

JSON mode is about syntax, not meaning. The model might return {"name": "Sony"} instead of {"product_name": "Sony"}. It might return "348" as a string instead of 348 as a number. You have a better chance that json.loads() succeeds, but you do not have a strong contract.

JSON mode is fine for quick experiments. Production code usually needs a schema.

Approach 2: Structured Outputs with JSON Schema

JSON Schema mode goes further. You give the API a schema that describes the exact object you want. OpenRouter supports this on compatible models through response_format with type: "json_schema".

How It Works: Constrained Decoding

For providers that support strict structured outputs, this is stronger than a prompt instruction. The decoder can restrict what the model is allowed to generate so the final answer follows the schema.

Still, schema compliance does not prove the facts are correct. A model can produce a valid price field and still put the wrong number in it.

main.py
Loading...

This is a much better contract than JSON mode. Your output has a defined shape, required fields, and no unexpected keys when additionalProperties is False. You should still validate business rules and, for important workflows, check the extracted values against the source.

Approach 3: Tool-Based Extraction (OpenAI Tools Format via OpenRouter)

Tool calling is another way to get structured data. You define a tool with an input schema, force the model to call that tool, and read the generated arguments.

This is useful when a model supports function calling well, or when you already use tools elsewhere in your application. Tool support varies by model, so check the selected model's supported parameters before relying on it.

How It Works

You define a tool with the shape you want. Then you tell the model to call that tool. In this extraction pattern, you do not actually execute the tool. You only read the arguments the model produced.

The result is still model output, so you still validate it. The advantage is that the output is organized as tool arguments rather than free-form prose.

main.py
Loading...

The key line is tool_choice={"type": "function", "function": {"name": "extract_product_review"}}. It asks the model to call that specific function, so the function arguments become the structured output.

Comparing Structured Output Approaches

Here is the practical difference between the three approaches.

Scroll
ApproachReliabilityComplexityBest For
JSON ModeMediumLowQuick prototypes, flexible schemas
JSON SchemaHighMediumStrict schema compliance, production use
Tool-BasedHighMediumModels that support function calling but not JSON Schema mode

Since the examples go through OpenRouter, all three approaches use the same OpenAI SDK client setup. The main difference is whether you pass response_format or tools, and whether the selected model supports that feature.

Use JSON Schema when you need a strict response contract and the model supports it. Use tool-based extraction when function calling is the better-supported path for your model or application. Use JSON mode for quick experiments, not for code that depends on exact field names and types.

Pydantic for Validation

So far, the examples have returned dictionaries. That is fine for a demo, but real applications need type checking, field validation, and clean serialization. This is where Pydantic helps.

If you are running these examples locally, install the validation libraries first:

Why Pydantic?

Pydantic is a Python validation library. You define a class with typed fields, and Pydantic checks incoming data against it. If the data is invalid, you get a useful error message.

For LLM applications, Pydantic solves three problems:

  1. Type safety: price should be a number, not a string that looks like a number.
  2. Validation rules: rating should be between 0 and 5, not 42.
  3. Missing field detection: If the model leaves out a required field, your code knows immediately.
main.py
Loading...

Field(ge=0, le=5) keeps the rating in range. Optional[str] means the reviewer name can be missing or null. These constraints protect your application from bad data before it reaches the rest of your system.

The Instructor Library

Writing the same parsing and validation code repeatedly gets old. The instructor library wraps an OpenAI-compatible client so you can pass a Pydantic model and receive a validated object.

main.py
Loading...

Instructor converts your Pydantic model into a schema, sends the request using a supported structured-output or tool-calling mode, parses the response, and validates it. If validation fails, it can retry with the validation error.

Because this goes through OpenRouter, the same pattern can work with other model families when they support the required structured-output mode. In this course, we will keep using GPT-5.4 Mini:

main.py
Loading...

The Pydantic model stays the same. The model choice still matters: unsupported structured-output modes, weaker extraction ability, or incompatible schema features can reduce reliability.

Schema-Driven Extraction

Structured output is useful when you need to extract entities and relationships from messy text without training a custom model.

Extracting Entities

Here is a practical example: extracting people and organizations from a short article.

main.py
Loading...

Extracting Relationships

You can also extract relationships between entities.

main.py
Loading...

This pattern gives your application a contract. The model proposes field values. Pydantic enforces the shape and the rules your code depends on.

Error Handling and Retry Strategies

Structured output reduces formatting failures. It does not remove all failures.

The model can misread a field, infer a value that was not present, or return a value that fails your business rules. In less constrained modes, it can also return malformed JSON. Your application needs a recovery path.

What Can Go Wrong

Here are the most common failure modes:

  1. Invalid JSON: The response is not parseable.
  2. Missing required fields: The schema requires a field the model omitted.
  3. Wrong types: A number comes back as a string, or a boolean comes back as "yes".
  4. Failed validation rules: A rating is 11 when the allowed maximum is 5.
  5. Unsupported facts: The model fills in a value that was not in the source text.

Retry with Error Feedback

For recoverable schema failures, catch the validation error and send it back as feedback. Specific errors are easier for the model to fix than a generic "try again."

For semantic failures, retries may not be enough. If the value must be correct, use source citations, deterministic checks, human review, or a narrower extraction design.

main.py
Loading...

Instructor's Built-In Retry

If you are using instructor, retry support is built in. Set max_retries and still log failures.

main.py
Loading...

When validation fails, instructor can feed the validation error back to the model and retry. That removes boilerplate, but it does not remove the need for limits, logging, and fallback behavior.

Choosing a Retry Strategy

Scroll
StrategyWhen to UseTradeoff
No retryStrict structured outputs plus simple schemasFastest path, but semantic mistakes are still possible
Retry with error feedbackJSON mode or tool-based extractionCosts extra API calls, but usually fixes the problem in 1-2 retries
Retry with re-promptingComplex extraction where the model misunderstands the taskMore expensive, but handles semantic errors
Fallback to different modelWhen primary model consistently failsAdds latency and complexity

A reasonable default is to use structured outputs for shape, Pydantic for business rules, max_retries=2, and logs that include enough source text and validation detail to debug failures.

Putting It All Together: Building a Resume Parser

Now combine the pieces into a resume parser. It takes raw resume text and returns a validated Pydantic model.

Step 1: Define the Schema

main.py
Loading...

Step 2: Build the Extractor

main.py
Loading...

Step 3: Test with Real Data

main.py
Loading...

Step 4: Handle Edge Cases

Real resumes are messy. Some have no email. Some list skills in paragraph form. Some contain gaps, overlapping roles, or unclear dates. Your Pydantic model handles some of this through optional fields and validators, but you still need a safe failure path.

main.py
Loading...

The resume parser pulls the chapter together: a Pydantic schema defines the contract, instructor handles the model call and retries, and parse_resume_safe prevents a bad input from crashing the caller. The same pattern works for invoices, support tickets, application forms, contracts, and many other extraction tasks.

One reminder is worth repeating: structured output improves shape, not truth. A schema can confirm that price is a number and rating falls between 0 and 5. It cannot prove the model read the source correctly. When a wrong value is costly, pair structured output with source citations, deterministic checks, or human review.

Quiz

Structured Output from LLMs Quiz

10 quizzes