Structured Outputs: JSON Schema Validation for LLMs
Ask an LLM for JSON and you will get broken JSON. Learn to constrain generation with Gemini response_schema and Pydantic, then verify the output as an explicit, catchable step.
Published on • August 3, 2026
AI Assistant

Structured outputs are the first line of verification: instead of hoping the model returns valid JSON, you constrain it and validate it.
Ask an LLM for JSON and you will, sooner or later, get Markdown-fenced JSON, a trailing comma, a key renamed, or a string where a number belongs. In a production pipeline every one of those failures is an uncaught error — “the failure cost: what happens when the AI is wrong and no one catches it.”
Structured outputs solve this at the source: the model is constrained to emit exactly the schema you declare, so parsing becomes deterministic. Validation then becomes a separate, explicit step — the core principle that an LLM is a resource in a value chain, not magic.
Prerequisites
- Python 3.10+
- A Gemini API key (
GEMINI_API_KEY) google-genaiandpydanticinstalled
Constrained generation with Gemini
Gemini supports structured outputs via response_mime_type="application/json" together with a response_schema:
from typing import Literal
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents="List the key metrics for the system health report.",
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema={
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["ok", "warning", "critical"]},
"latency_p50_ms": {"type": "number"},
"error_rate_percent": {"type": "number"},
},
"required": ["status", "latency_p50_ms", "error_rate_percent"],
},
),
)
print(response.text)
Because the schema is declared, Gemini responds with exactly status, latency_p50_ms, and error_rate_percent, in the right types, so you can parse with json.loads safely.
Defining the schema with Pydantic
For anything nontrivial, define the schema in Pydantic and convert it to a Gemini schema:
from typing import Literal
from pydantic import BaseModel, Field
from google.genai import types
class Incident(BaseModel):
severity: Literal["low", "med", "high", "critical"]
service: str = Field(description="Name of the affected service")
summary: str = Field(description="One-sentence summary")
related_ids: list[int] = []
config = types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=Incident,
)
When the response returns, parse it straight into an Incident object and benefit from Pydantic’s validation and type coercion.
Recovering a schema from samples with genson
You don’t always have a schema in mind. When you want to constrain output to match the structure recovered from free-form samples, genson recovers it offline:
from genson import SchemaBuilder
examples = [
{"name": "api", "hitsPerSec": 120},
{"name": "db", "hitsPerSec": 300},
]
builder = SchemaBuilder()
for ex in examples:
builder.add_object(ex)
schema = builder.to_schema() # a JSON Schema over the observed samples
Guarding the output (the verification step)
Structured output + a final validation is the “explicit verification step” to design in:
import json
from pydantic import ValidationError
def parse_incident(text: str) -> Incident | None:
try:
return Incident.model_validate(json.loads(text))
except (json.JSONDecodeError, ValidationError) as e:
raise VerificationError("structured output failed validation") from e
The decision to re-prompt, route to human review, or drop the record belongs to your governance policy — but it is now an explicit, catchable step.
Putting It All Together
A small function that returns a constrained, validated health report:
from typing import Literal
from pydantic import BaseModel
from google import genai
from google.genai import types
client = genai.Client()
class HealthReport(BaseModel):
status: Literal["ok", "warning", "critical"]
latency_p50_ms: float
error_rate_percent: float
def report(prompt: str) -> HealthReport:
resp = client.models.generate_content(
model="gemini-2.5-pro",
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=HealthReport,
),
)
health = HealthReport.model_validate(json.loads(resp.text))
return health
Conclusion & Next Steps
Structured outputs remove the most common class of brittle LLM bugs. Next: add retry-on-invalid where a validation error is fed back as a corrective prompt, and wire schema checks into your downstream contract tests so a change in shape fails loudly rather than silently. — verification must be built in, not hoped for.
References / Sources
- Gemini structured outputs. https://ai.google.dev/gemini-api/docs/structured-output
- Pydantic v2. https://docs.pydantic.dev/latest/
- genson JSON schema builder. https://github.com/wolverdude/GenSON