| 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
uv add llmring
pip install llmring
Provider SDKs (install what you need):
uv add openai>=1.0
uv add anthropic>=0.67
uv add google-genai
uv add ollama>=0.4
API Overview
This skill covers:
response_format parameter in LLMRequest
- JSON Schema definition
strict mode for validation
parsed field in LLMResponse
- Cross-provider compatibility
Quick Start
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="extractor",
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)
Complete API Documentation
response_format Parameter
The response_format parameter controls structured output.
Structure:
response_format = {
"type": "json_schema",
"json_schema": {
"name": str,
"schema": dict
},
"strict": bool
}
Parameters:
type (str, required): Must be "json_schema"
json_schema (dict, required): Schema definition
name (str, required): Name for the schema
schema (dict, required): JSON Schema defining the structure
strict (bool, optional): If true, strictly enforce schema
Example:
from llmring import LLMRequest, Message
request = LLMRequest(
model="extractor",
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:
{"type": "string"}
{"type": "number"}
{"type": "integer"}
{"type": "boolean"}
{
"type": "array",
"items": {"type": "string"}
}
{
"type": "object",
"properties": {
"field1": {"type": "string"},
"field2": {"type": "integer"}
},
"required": ["field1"]
}
Example Schemas:
person_schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"email": {"type": "string"},
"is_active": {"type": "boolean"}
},
"required": ["name", "age"]
}
list_schema = {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"title": {"type": "string"}
}
}
}
}
}
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 string
parsed (dict): Parsed Python dictionary (ready to use)
model (str): Model used
usage (dict): Token usage
finish_reason (str): Completion reason
Example:
response = await service.chat(request)
json_string = response.content
data = response.parsed
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:
request = LLMRequest(
model="extractor",
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",
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",
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",
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:
request = LLMRequest(
model="extractor",
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)
Streaming Structured Output
from llmring import LLMRing, LLMRequest, Message
async with LLMRing() as service:
request = LLMRequest(
model="extractor",
messages=[Message(role="user", content="Generate JSON data")],
response_format={
"type": "json_schema",
"json_schema": {
"name": "data",
"schema": {
"type": "object",
"properties": {
"result": {"type": "string"}
}
}
}
}
)
full_json = ""
async for chunk in service.chat_stream(request):
print(chunk.delta, end="", flush=True)
full_json += chunk.delta
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 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 |
| Google | 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
import json
response = await service.chat(request)
data = json.loads(response.content)
Right: Use Parsed Field
response = await service.chat(request)
data = response.parsed
Wrong: Missing Required Fields
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
}
}
Right: Specify Required Fields
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
Wrong: Wrong Type Names
schema = {
"type": "object",
"properties": {
"value": {"type": "int"}
}
}
Right: Use Correct JSON Schema Types
schema = {
"type": "object",
"properties": {
"text": {"type": "string"},
"count": {"type": "integer"},
"price": {"type": "number"},
"active": {"type": "boolean"}
}
}
Wrong: Not Handling Errors
response = await service.chat(request)
name = response.parsed["name"]
Right: 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",
messages=[Message(role="user", content="Analyze this data")],
tools=[...],
response_format={
"type": "json_schema",
"json_schema": {
"name": "analysis",
"schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
"score": {"type": "number"}
}
}
}
}
)
response = await service.chat(request)
if response.tool_calls:
pass
elif response.parsed:
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 output
llmring-streaming - Stream structured JSON construction
llmring-tools - Combine with function calling
llmring-lockfile - Configure models for structured output
llmring-providers - Provider-specific schema features