Prompt-only "return JSON" extraction shows double-digit parse-failure rates in production. This playbook builds the three-layer reliability stack that experienced teams ship: (1) schema-enforced generation at the API level, (2) Pydantic validation with automatic retry, (3) constrained decoding as the fallback for open-weights models.
Tier: Core · Workflow area: Structured Outputs & LLM APIs
The output is an extraction function you can put behind a production endpoint and trust.
JSONDecodeError or silently dropping fields in production.Skip this for exploratory notebook work where you will eyeball every output anyway.
pydantic>=2, anthropic (or openai), optionally instructor and outlines.Define the schema as a Pydantic model, not a prompt paragraph. Field types, enums, and descriptions are your contract:
from pydantic import BaseModel, Field
from typing import Literal
class Ticket(BaseModel):
product: str = Field(description="Product name as mentioned, verbatim")
severity: Literal["low", "medium", "high", "critical"]
churn_risk: bool
summary: str = Field(description="One sentence, max 20 words")
Structured-output APIs require additionalProperties: false and do not support numeric/length constraints server-side — the SDKs strip those and validate them client-side, so keep Field(ge=..., max_length=...) for the client-side pass.
Layer 1 — schema-enforced generation. Use the API's native parse method so the schema is enforced at decode time, not hoped for:
import anthropic
client = anthropic.Anthropic()
resp = client.messages.parse(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": f"Extract ticket fields:\n\n{raw_text}"}],
output_format=Ticket,
)
ticket = resp.parsed_output # a validated Ticket instance
Layer 2 — validation-retry with Instructor. For semantic constraints the schema cannot express (e.g. "summary must not copy the input verbatim"), add a Pydantic validator and let Instructor feed the validation error back to the model:
import instructor, anthropic
client = instructor.from_anthropic(anthropic.Anthropic())
ticket = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=[{"role": "user", "content": raw_text}],
response_model=Ticket,
max_retries=2, # re-prompts with the validator's error message
)
Layer 3 — constrained decoding fallback (local/open-weights). When the input cannot leave your infrastructure, Outlines masks invalid tokens at generation time so the output is grammatically guaranteed to match the schema:
import outlines
model = outlines.models.transformers("Qwen/Qwen2.5-7B-Instruct")
generator = outlines.generate.json(model, Ticket)
ticket = generator(prompt)
Measure before shipping. Run all three layers over your 20–50 sample inputs and log: parse-success rate, per-field accuracy against a hand-labeled subset, retries per call, and latency. Structured outputs guarantee shape, not truth — field-level accuracy is the number that matters.
Schema enforcement eliminates parse failures; it does nothing about wrong values. Budget your eval effort accordingly: shape is solved at the API layer, correctness is measured with labeled samples.
client.messages.parse(..., output_format=Model) → .parsed_output; raw form is output_config={"format": {"type": "json_schema", "schema": ...}}. For tool-calling pipelines, strict: true on the tool definition guarantees tool_use.input validates against the schema.client.responses.parse(model=..., input=[...], text_format=Model) → response.output_parsed (Responses API), or client.chat.completions.parse(..., response_format=Model) → .choices[0].message.parsed.from_anthropic / from_openai / from_provider) gives the same response_model + max_retries interface across providers — useful when you must stay vendor-neutral.guided_json=<schema> per request.Literal["a","b","c"] forces the model to pick one even when none applies. Add "unknown" to every enum and treat its frequency as a data-quality metric.str | None everywhere gives you 100% parse success and empty rows. Make fields required unless absence is genuinely meaningful.max_retries multiplies cost and latency; cap at 2 and route persistent failures to a dead-letter queue for review.For client data that cannot reach a hosted API (standard for India services-company engagements):
Constrained decoding guarantees valid JSON even when the model is completely wrong — a hallucinated but schema-valid record is more dangerous than a parse error because nothing crashes. Keep the labeled-sample accuracy check in the local path too.
"unknown" enum rates logged as metrics, not swallowed.