structured-output
Use when extracting structured data from LLMs, parsing JSON responses, or enforcing output schemas - unified JSON schema API works across OpenAI, Anthropic, Google, and Ollama with automatic validation and parsing
When & Why to Use This Skill
The Structured Output skill provides a unified JSON Schema API to extract validated, structured data from leading LLM providers including OpenAI, Anthropic, Google Gemini, and Ollama. It eliminates the need for manual parsing and complex regex by enforcing strict output schemas, ensuring that AI-generated responses are consistently formatted as Python dictionaries for seamless programmatic integration.
Use Cases
- Automated Data Extraction: Efficiently extract structured entities like contact information, dates, and product specifications from unstructured text directly into database-ready formats.
- Reliable Content Classification: Perform sentiment analysis or support ticket triaging by forcing the LLM to choose from predefined enum values with guaranteed schema compliance.
- Complex Object Generation: Create deeply nested JSON structures for blog posts, user profiles, or technical reports that require specific mandatory fields and data types.
- Cross-Model Compatibility: Build provider-agnostic applications where the same JSON Schema logic works across different models (e.g., switching from OpenAI to Anthropic) without changing parsing code.
| name | structured-output |
|---|---|
| description | Use when extracting structured data from LLMs, parsing JSON responses, or enforcing output schemas - unified JSON schema API works across OpenAI, Anthropic, Google, and Ollama with automatic validation and parsing |
Structured Output with JSON Schema
Installation
# With uv (recommended)
uv add llmring
# With pip
pip install llmring
Provider SDKs (install what you need):
uv add openai>=1.0 # OpenAI
uv add anthropic>=0.67 # Anthropic
uv add google-genai # Google Gemini
uv add ollama>=0.4 # Ollama
API Overview
This skill covers:
response_formatparameter in LLMRequest- JSON Schema definition
strictmode for validationparsedfield in LLMResponse- Cross-provider compatibility
Quick Start
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
# Define JSON schema
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(role="user", content="Generate a person")],
response_format={
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"email": {"type": "string"}
},
"required": ["name", "age"]
}
},
"strict": True
}
)
response = await service.chat(request)
print("JSON string:", response.content)
print("Parsed data:", response.parsed) # Python dict
Complete API Documentation
response_format Parameter
The response_format parameter controls structured output.
Structure:
response_format = {
"type": "json_schema",
"json_schema": {
"name": str, # Schema name
"schema": dict # JSON Schema definition
},
"strict": bool # Optional: enforce strict validation (at response_format level)
}
Parameters:
type(str, required): Must be"json_schema"json_schema(dict, required): Schema definitionname(str, required): Name for the schemaschema(dict, required): JSON Schema defining the structurestrict(bool, optional): If true, strictly enforce schema
Example:
from llmring import LLMRequest, Message
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(role="user", content="Generate data")],
response_format={
"type": "json_schema",
"json_schema": {
"name": "response",
"schema": {
"type": "object",
"properties": {
"answer": {"type": "string"}
},
"required": ["answer"]
}
},
"strict": True
}
)
JSON Schema Format
JSON Schema defines the expected structure.
Basic Types:
# String
{"type": "string"}
# Number (integer or float)
{"type": "number"}
# Integer only
{"type": "integer"}
# Boolean
{"type": "boolean"}
# Array
{
"type": "array",
"items": {"type": "string"} # Array of strings
}
# Object
{
"type": "object",
"properties": {
"field1": {"type": "string"},
"field2": {"type": "integer"}
},
"required": ["field1"] # Required fields
}
Example Schemas:
# Person schema
person_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"email": {"type": "string"},
"is_active": {"type": "boolean"}
},
"required": ["name", "age"]
}
# List of items schema
list_schema = {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"title": {"type": "string"}
}
}
}
}
}
# Nested object schema
nested_schema = {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
}
}
}
}
}
}
LLMResponse with Structured Output
When using response_format, the response contains both raw JSON and parsed data.
Attributes:
content(str): Raw JSON stringparsed(dict): Parsed Python dictionary (ready to use)model(str): Model usedusage(dict): Token usagefinish_reason(str): Completion reason
Example:
response = await service.chat(request)
# Both available:
json_string = response.content # '{"name": "Alice", "age": 30}'
data = response.parsed # {"name": "Alice", "age": 30}
# Use parsed data directly
print(f"Name: {data['name']}")
print(f"Age: {data['age']}")
Common Patterns
Extracting Structured Data
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
# Extract contact info from text
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(
role="user",
content="Extract contact info: John Smith, age 35, email john@example.com"
)],
response_format={
"type": "json_schema",
"json_schema": {
"name": "contact",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"email": {"type": "string"}
},
"required": ["name"]
}
},
"strict": True
}
)
response = await service.chat(request)
contact = response.parsed
print(f"Name: {contact['name']}, Age: {contact['age']}")
Generating Lists
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(
role="user",
content="List 5 programming languages with their release years"
)],
response_format={
"type": "json_schema",
"json_schema": {
"name": "languages",
"schema": {
"type": "object",
"properties": {
"languages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"year": {"type": "integer"}
},
"required": ["name", "year"]
}
}
},
"required": ["languages"]
}
}
}
)
response = await service.chat(request)
for lang in response.parsed["languages"]:
print(f"{lang['name']}: {lang['year']}")
Classification Tasks
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(
role="user",
content="Classify sentiment: This product is amazing!"
)],
response_format={
"type": "json_schema",
"json_schema": {
"name": "sentiment",
"schema": {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"]
},
"confidence": {
"type": "number",
"minimum": 0.0,
"maximum": 1.0
}
},
"required": ["sentiment", "confidence"]
}
},
"strict": True
}
)
response = await service.chat(request)
result = response.parsed
print(f"Sentiment: {result['sentiment']} ({result['confidence']:.2f})")
Nested Structures
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(
role="user",
content="Generate a blog post with title, author info, and tags"
)],
response_format={
"type": "json_schema",
"json_schema": {
"name": "blog_post",
"schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"author": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
},
"required": ["name"]
},
"tags": {
"type": "array",
"items": {"type": "string"}
},
"content": {"type": "string"}
},
"required": ["title", "author", "content"]
}
}
}
)
response = await service.chat(request)
post = response.parsed
print(f"Title: {post['title']}")
print(f"By: {post['author']['name']}")
print(f"Tags: {', '.join(post['tags'])}")
Validation with Enums
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
# Enforce specific values
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(
role="user",
content="What's the priority of this bug?"
)],
response_format={
"type": "json_schema",
"json_schema": {
"name": "bug_priority",
"schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
},
"reasoning": {"type": "string"}
},
"required": ["priority"]
}
},
"strict": True
}
)
response = await service.chat(request)
# priority is guaranteed to be one of the enum values
Streaming Structured Output
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(role="user", content="Generate JSON data")],
response_format={
"type": "json_schema",
"json_schema": {
"name": "data",
"schema": {
"type": "object",
"properties": {
"result": {"type": "string"}
}
}
}
}
)
# Stream JSON construction
full_json = ""
async for chunk in service.chat_stream(request):
print(chunk.delta, end="", flush=True)
full_json += chunk.delta
# Parse final JSON
import json
data = json.loads(full_json)
print(f"\nParsed: {data}")
Strict Mode
When strict: True, the schema is strictly enforced:
response_format = {
"type": "json_schema",
"json_schema": {
"name": "data",
"schema": {...}
},
"strict": True # Strict validation at response_format level
}
Strict mode guarantees:
- Output matches schema exactly
- All required fields present
- No extra fields
- Correct types
- Enum values respected
Without strict mode:
- Best-effort schema following
- May have extra fields
- Types usually correct but not guaranteed
Provider Support
| Provider | JSON Schema | Strict Mode | Notes |
|---|---|---|---|
| OpenAI | Yes | Yes | Native support |
| Anthropic | Yes | Yes | Adapted automatically |
| Yes | Yes | Adapted automatically | |
| Ollama | Yes | Best-effort | Prompt-based adaptation |
LLMRing automatically adapts JSON schema to each provider's format.
Common Mistakes
Wrong: Not Using Parsed Field
# DON'T DO THIS - manually parse JSON
import json
response = await service.chat(request)
data = json.loads(response.content) # Unnecessary
Right: Use Parsed Field
# DO THIS - use pre-parsed data
response = await service.chat(request)
data = response.parsed # Already a dict
Wrong: Missing Required Fields
# DON'T DO THIS - forgot to mark required fields
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
}
# No "required" field!
}
Right: Specify Required Fields
# DO THIS - mark required fields
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"] # Both required
}
Wrong: Wrong Type Names
# DON'T DO THIS - invalid type
schema = {
"type": "object",
"properties": {
"value": {"type": "int"} # Wrong! Should be "integer"
}
}
Right: Use Correct JSON Schema Types
# DO THIS - correct types
schema = {
"type": "object",
"properties": {
"text": {"type": "string"},
"count": {"type": "integer"}, # Not "int"
"price": {"type": "number"}, # For floats
"active": {"type": "boolean"} # Not "bool"
}
}
Wrong: Not Handling Errors
# DON'T DO THIS - assume parsing always works
response = await service.chat(request)
name = response.parsed["name"] # May fail!
Right: Handle Missing Fields
# DO THIS - handle missing fields
response = await service.chat(request)
name = response.parsed.get("name", "Unknown")
if "age" in response.parsed:
age = response.parsed["age"]
Combining with Tools
You can use structured output and tools together:
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="extractor", # Your alias for structured extraction
messages=[Message(role="user", content="Analyze this data")],
tools=[...], # Define tools
response_format={ # Also request structured output
"type": "json_schema",
"json_schema": {
"name": "analysis",
"schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
"score": {"type": "number"}
}
}
}
}
)
response = await service.chat(request)
# May have tool_calls OR parsed JSON
if response.tool_calls:
# Handle tool execution
pass
elif response.parsed:
# Handle structured output
print(response.parsed)
Best Practices
- Always specify required fields: Don't rely on optional fields
- Use enums for constrained values: Ensures valid outputs
- Use strict mode in production: Guarantees schema compliance
- Provide clear property descriptions: Helps model understand intent
- Start simple: Test with basic schemas before complex nested structures
- Use parsed field: Don't manually parse JSON strings
- Handle missing fields gracefully: Use
.get()with defaults
Related Skills
llmring-chat- Basic chat without structured outputllmring-streaming- Stream structured JSON constructionllmring-tools- Combine with function callingllmring-lockfile- Configure models for structured outputllmring-providers- Provider-specific schema features