Validate LLM JSON Output Against a Schema
You got valid JSON from the LLM. But is it the right JSON? Missing fields, wrong types, bad enum values — schema validation catches what json.loads() can't.
The gap between "valid JSON" and "correct JSON"
json.loads() only checks syntax. It doesn't know your age field should be an integer, or that status must be one of three values. Syntactically valid JSON can still break your downstream code.
import json # This parses without error: data = json.loads('{"name": "Alice", "age": "twenty-five", "status": "maybe"}') # But your API expects: # age → integer, not string # status → "active" | "inactive", not "maybe"
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0},
"status": {"type": "string", "enum": ["active", "inactive"]}
},
"required": ["name", "age", "status"]
}
age should be an integer, they sometimes return "25" (a string) instead of 25 (a number). And enum values? Models love inventing new ones.
Approach 1: Pydantic validation
Define your expected structure as a Pydantic model and let it validate (and coerce) the parsed data. This is the most popular Python approach.
from pydantic import BaseModel from enum import Enum import json class Status(str, Enum): active = "active" inactive = "inactive" class User(BaseModel): name: str age: int status: Status active: bool = True data = json.loads(llm_output) user = User(**data) # ValidationError if age is not coercible to int # Pydantic v2 coerces "25" → 25 automatically # But "twenty-five" still raises ValidationError
- Type coercion built in (v2)
- Rich error messages
- Python ecosystem standard
- Python only
- No auto-retry on failure
- Doesn't fix syntax errors
Approach 2: jsonschema validation
The jsonschema library validates against a standard JSON Schema document. Language-agnostic schema definitions that work in Python, JavaScript, Go, or anywhere.
import jsonschema import json schema = { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer", "minimum": 0}, "status": {"type": "string", "enum": ["active", "inactive"]} }, "required": ["name", "age", "status"] } data = json.loads(llm_output) jsonschema.validate(data, schema) # raises ValidationError on mismatch # ♥ Strict: "25" (string) FAILS the integer check # ♥ Enum: "maybe" FAILS the enum check
- Industry standard schema format
- Language-agnostic definitions
- Strict type checking
- No type coercion ("25" fails)
- No auto-retry on failure
- No syntax repair
Approach 3: Contract Mode (StreamFix)
Instead of validating after the fact, enforce the schema at the proxy layer. StreamFix repairs syntax first (markdown fences, Python literals, trailing commas), then coerces types, then validates against your schema — and auto-retries once if validation fails.
from openai import OpenAI client = OpenAI( api_key="sk_YOUR_STREAMFIX_KEY", base_url="https://streamfix.dev/v1", ) response = client.chat.completions.create( model="openai/gpt-4o-mini", messages=[{"role": "user", "content": "Return user data as JSON"}], extra_body={ "schema": { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer", "minimum": 0}, "status": {"type": "string", "enum": ["active", "inactive"]} }, "required": ["name", "age", "status"] } }, ) data = response.choices[0].message.content # ✅ guaranteed schema-valid JSON
# Access via with_raw_response: raw = client.chat.completions.with_raw_response.create(...) print(raw.headers["X-StreamFix-Schema-Valid"]) # "true" print(raw.headers["X-StreamFix-Applied"]) # "fence_strip, type_coerce" print(raw.headers["X-StreamFix-Retried"]) # "false"
True/None), coerce types to match your schema, validate. If validation still fails, it retries the LLM call once with a corrective prompt. Your code only sees the final, validated result.
Side-by-side comparison
| Feature | Pydantic | jsonschema | Contract Mode |
|---|---|---|---|
| Type coercion ("25" → 25) | Yes (v2) | No | Yes |
| Enum validation | Yes | Yes | Yes |
| Auto-retry on failure | No | No | Yes (1 retry) |
| Syntax repair | No | No | Yes |
| Language | Python | Any | Any (HTTP proxy) |
| Extra LLM call | No | No | Only on retry |
Pydantic and jsonschema are client-side libraries. Contract Mode runs server-side at the proxy layer before your code sees the response.
Schema-guaranteed JSON from any LLM
Pass a schema in extra_body and StreamFix guarantees the response matches — repairing, coercing, and retrying as needed. Works with any model, any language, one base_url change.
from openai import OpenAI client = OpenAI( api_key="sk_YOUR_STREAMFIX_KEY", base_url="https://streamfix.dev/v1", ) resp = client.chat.completions.create( model="openai/gpt-4o-mini", messages=[{"role": "user", "content": "Return user data as JSON"}], extra_body={ "schema": { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer", "minimum": 0}, "status": {"type": "string", "enum": ["active", "inactive"]} }, "required": ["name", "age", "status"] } }, ) data = json.loads(resp.choices[0].message.content) # ✅ always valid