The problem with unstructured AI output
AI models return free-form text. If your app expects JSON, the model might return Here's the JSON: {...} with extra text, or worse — malformed JSON. Here are three reliable patterns to get structured output every time.
Pattern 1: JSON mode (OpenAI-compatible)
from openai import OpenAI
client = OpenAI(
api_key="izzi-YOUR_KEY_HERE",
base_url="https://api.izziapi.com/v1"
)
response = client.chat.completions.create(
model="gpt-5.4",
response_format={"type": "json_object"},
messages=[{
"role": "user",
"content": 'Extract entities from this text and return as JSON with keys "people", "places", "dates": The meeting with John in Paris on March 15th...'
}]
)
import json
data = json.loads(response.choices[0].message.content)
print(data["people"]) # ["John"]
print(data["places"]) # ["Paris"]Pattern 2: Tool use (Claude — most reliable)
import anthropic
client = anthropic.Anthropic(
api_key="izzi-YOUR_KEY_HERE",
base_url="https://api.izziapi.com/anthropic"
)
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
tools=[{
"name": "extract_info",
"description": "Extract structured information from text",
"input_schema": {
"type": "object",
"properties": {
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
"topics": {"type": "array", "items": {"type": "string"}},
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
},
"required": ["sentiment", "topics", "confidence"]
}
}],
tool_choice={"type": "tool", "name": "extract_info"},
messages=[{
"role": "user",
"content": "Analyze: The new API is incredibly fast but the docs are confusing."
}]
)
# Tool use guarantees valid JSON matching the schema
result = response.content[0].input
print(result)
# {"sentiment": "neutral", "topics": ["API", "performance", "documentation"], "confidence": 0.85}Pattern 3: Pydantic validation (safety net)
from pydantic import BaseModel, Field
from openai import OpenAI
import json
class CodeReview(BaseModel):
severity: str = Field(description="critical, major, minor, or nitpick")
file: str = Field(description="File path")
line: int = Field(description="Line number")
issue: str = Field(description="What's wrong")
fix: str = Field(description="How to fix it")
def get_structured_review(code: str) -> list[CodeReview]:
response = client.chat.completions.create(
model="claude-sonnet-4-20250514",
messages=[{
"role": "user",
"content": f"Review this code. Return a JSON array of issues: {code}"
}],
max_tokens=2000
)
raw = response.choices[0].message.content
# Strip markdown code fences if present
raw = raw.strip().removeprefix("```json").removesuffix("```").strip()
issues = json.loads(raw)
return [CodeReview(**issue) for issue in issues]Which pattern to use
| Pattern | Reliability | Model support | Best for |
|---|---|---|---|
| JSON mode | High | GPT-5, Gemini | Simple JSON output |
| Tool use | Highest | Claude (best), GPT-5 | Guaranteed schema compliance |
| Pydantic validation | Medium + safety | Any model | Post-processing validation |
