# 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

- Skill: `tomevault-io/structured-output` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/structured-output`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/structured-output/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/structured-output

---


# Structured Output with JSON Schema

## Installation

```bash
# With uv (recommended)
uv add llmring

# With pip
pip install llmring
```

**Provider SDKs (install what you need):**
```bash
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_format` parameter in LLMRequest
- JSON Schema definition
- `strict` mode for validation
- `parsed` field in LLMResponse
- Cross-provider compatibility

## Quick Start

```python
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:**
```python
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 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:**
```python
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:**
```python
# 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:**

```python
# 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 string
- `parsed` (dict): Parsed Python dictionary (ready to use)
- `model` (str): Model used
- `usage` (dict): Token usage
- `finish_reason` (str): Completion reason

**Example:**
```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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:

```python
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 |
| **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

```python
# DON'T DO THIS - manually parse JSON
import json
response = await service.chat(request)
data = json.loads(response.content)  # Unnecessary
```

**Right: Use Parsed Field**

```python
# DO THIS - use pre-parsed data
response = await service.chat(request)
data = response.parsed  # Already a dict
```

### Wrong: Missing Required Fields

```python
# 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**

```python
# DO THIS - mark required fields
schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer"}
    },
    "required": ["name", "age"]  # Both required
}
```

### Wrong: Wrong Type Names

```python
# DON'T DO THIS - invalid type
schema = {
    "type": "object",
    "properties": {
        "value": {"type": "int"}  # Wrong! Should be "integer"
    }
}
```

**Right: Use Correct JSON Schema Types**

```python
# 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

```python
# DON'T DO THIS - assume parsing always works
response = await service.chat(request)
name = response.parsed["name"]  # May fail!
```

**Right: Handle Missing Fields**

```python
# 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:

```python
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

1. **Always specify required fields:** Don't rely on optional fields
2. **Use enums for constrained values:** Ensures valid outputs
3. **Use strict mode in production:** Guarantees schema compliance
4. **Provide clear property descriptions:** Helps model understand intent
5. **Start simple:** Test with basic schemas before complex nested structures
6. **Use parsed field:** Don't manually parse JSON strings
7. **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

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/juanre) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-13 -->

