Agent skill
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
Install this agent skill to your Project
npx add-skill https://github.com/juanre/llmring/tree/main/skills/structured-output
SKILL.md
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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
providers
Use when switching between LLM providers, accessing provider-specific features (Anthropic caching, OpenAI logprobs), or using raw SDK clients - covers multi-provider patterns and direct SDK access for OpenAI, Anthropic, Google, and Ollama
tools
Use when implementing function calling, tool use, or agents with LLMs - unified tool API works across OpenAI, Anthropic, Google, and Ollama with consistent tool definition and execution patterns
streaming
Use when building real-time chat interfaces, displaying incremental LLM responses, or streaming output from OpenAI, Anthropic, Google, or Ollama - async iteration with usage tracking works across all providers
lockfile
Use when creating llmring.lock file for new project (REQUIRED for all applications), configuring model aliases with semantic task-based names, managing environment-specific profiles (dev/staging/prod), or setting up fallback models - lockfile creation is mandatory first step, bundled lockfile is only for llmring tools
chat
Use when starting a new project with llmring, building an application using LLMs, making basic chat completions, or sending messages to OpenAI, Anthropic, Google, or Ollama - covers lockfile creation (MANDATORY first step), semantic alias usage, unified interface for all providers with consistent message structure and response handling
edit-article
Edit and improve articles by restructuring sections, improving clarity, and tightening prose. Use when user wants to edit, revise, or improve an article draft.
Didn't find tool you were looking for?