Structured Output
Structured output lets you make model responses predictable, machine-readable, and easier to validate in your application.
Instead of asking the model to “please respond in JSON” and hoping it follows instructions, you can explicitly request structured output through the chat completions API. This is essential for production systems that need reliable parsing, deterministic schemas, and downstream automation.
SolRouter supports OpenAI-compatible structured output through the standard chat completions endpoint.
Base URL
https://api.solrouter.io/ai
Why structured output matters
Plain-text responses are great for chat, but they are not ideal when your application needs to:
- extract fields from documents
- classify content into categories
- generate typed objects for APIs
- return records for databases
- produce tool-ready payloads
- validate model output before further processing
Without structured output, you often end up writing brittle prompt instructions and fragile parsers.
With structured output, you can:
- force JSON output
- enforce a schema
- reject invalid fields
- reduce parsing errors
- simplify backend integration
Two structured output modes
SolRouter supports two main approaches:
| Mode | Description | Best for |
|---|---|---|
json_object | Forces the model to return a valid JSON object | simple extraction, lightweight automation |
json_schema | Constrains output to a JSON Schema | production integrations, typed data, strict validation |
json_object
json_object is the simpler option. It tells the model to respond with a valid JSON object.
Example request
{
"model": "openai/gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Extract the title and summary from this article."
}
],
"response_format": {
"type": "json_object"
}
}
Example response
{
"id": "chatcmpl_123",
"object": "chat.completion",
"model": "openai/gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"title\":\"How Routing Layers Improve Reliability\",\"summary\":\"The article explains how routing layers improve uptime, cost control, and model flexibility.\"}"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 41,
"completion_tokens": 29,
"total_tokens": 70,
"cost": 0.0000048
}
}
When to use json_object
Use json_object when:
- you want valid JSON
- the shape is simple
- minor variation in field presence is acceptable
- you do not need strict schema enforcement
Limitations of json_object
json_object improves reliability, but it does not fully enforce field structure. The model may still:
- omit fields
- rename fields
- add extra keys
- use unexpected value formats
If the exact shape matters, prefer json_schema.
json_schema
json_schema lets you provide an explicit JSON Schema for the model to follow.
This is the best choice when you need stable, validated output for production systems.
Example request
{
"model": "openai/gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Extract invoice information from this text: Invoice #INV-1042, total $184.50, currency USD."
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "invoice_extraction",
"schema": {
"type": "object",
"properties": {
"invoice_number": {
"type": "string"
},
"total": {
"type": "number"
},
"currency": {
"type": "string"
}
},
"required": ["invoice_number", "total", "currency"],
"additionalProperties": false
}
}
}
}
Example response
{
"choices": [
{
"message": {
"role": "assistant",
"content": "{\"invoice_number\":\"INV-1042\",\"total\":184.5,\"currency\":\"USD\"}"
},
"finish_reason": "stop"
}
]
}
Why json_schema is better for production
With a schema, you can define:
- exact field names
- required vs optional fields
- string, number, boolean, array, and object types
- enums
- nested objects
- whether extra properties are allowed
This makes downstream code much safer.
TypeScript example
Using the OpenAI SDK
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.solrouter.io/ai",
apiKey: process.env.SOLROUTER_API_KEY,
});
const completion = await client.chat.completions.create({
model: "openai/gpt-4o-mini",
messages: [
{
role: "user",
content: "Extract name, email, and company from: Sarah Chen, sarah@acme.io, Acme Labs",
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "contact_record",
schema: {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
company: { type: "string" },
},
required: ["name", "email", "company"],
additionalProperties: false,
},
},
},
});
const raw = completion.choices[0].message.content ?? "{}";
const data = JSON.parse(raw);
console.log(data.name);
console.log(data.email);
console.log(data.company);
With runtime validation using Zod
import OpenAI from "openai";
import { z } from "zod";
const ContactSchema = z.object({
name: z.string(),
email: z.string().email(),
company: z.string(),
});
const client = new OpenAI({
baseURL: "https://api.solrouter.io/ai",
apiKey: process.env.SOLROUTER_API_KEY,
});
const completion = await client.chat.completions.create({
model: "openai/gpt-4o-mini",
messages: [
{
role: "user",
content: "Extract name, email, and company from: Sarah Chen, sarah@acme.io, Acme Labs",
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "contact_record",
schema: {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
company: { type: "string" },
},
required: ["name", "email", "company"],
additionalProperties: false,
},
},
},
});
const raw = completion.choices[0].message.content ?? "{}";
const parsed = ContactSchema.parse(JSON.parse(raw));
console.log(parsed);
Even with schema-constrained output, runtime validation is still a good practice.
Python example
Using the OpenAI SDK
from openai import OpenAI
import json
import os
client = OpenAI(
base_url="https://api.solrouter.io/ai",
api_key=os.environ["SOLROUTER_API_KEY"],
)
completion = client.chat.completions.create(
model="openai/gpt-4o-mini",
messages=[
{
"role": "user",
"content": "Extract product name, price, and currency from: SolRouter Pro Plan, $19.99, USD"
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "product_record",
"schema": {
"type": "object",
"properties": {
"product_name": {"type": "string"},
"price": {"type": "number"},
"currency": {"type": "string"}
},
"required": ["product_name", "price", "currency"],
"additionalProperties": False
}
}
}
)
raw = completion.choices[0].message.content or "{}"
data = json.loads(raw)
print(data["product_name"])
print(data["price"])
print(data["currency"])
With Pydantic validation
from openai import OpenAI
from pydantic import BaseModel
import json
import os
class ProductRecord(BaseModel):
product_name: str
price: float
currency: str
client = OpenAI(
base_url="https://api.solrouter.io/ai",
api_key=os.environ["SOLROUTER_API_KEY"],
)
completion = client.chat.completions.create(
model="openai/gpt-4o-mini",
messages=[
{
"role": "user",
"content": "Extract product name, price, and currency from: SolRouter Pro Plan, $19.99, USD"
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "product_record",
"schema": {
"type": "object",
"properties": {
"product_name": {"type": "string"},
"price": {"type": "number"},
"currency": {"type": "string"}
},
"required": ["product_name", "price", "currency"],
"additionalProperties": False
}
}
}
)
raw = completion.choices[0].message.content or "{}"
parsed = ProductRecord.model_validate(json.loads(raw))
print(parsed)
fetch example
const response = await fetch("https://api.solrouter.io/ai/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SOLROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai/gpt-4o-mini",
messages: [
{
role: "user",
content: "Extract invoice number and total from: Invoice 2207, amount $45.80",
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "invoice",
schema: {
type: "object",
properties: {
invoice_number: { type: "string" },
total: { type: "number" },
},
required: ["invoice_number", "total"],
additionalProperties: false,
},
},
},
}),
});
const data = await response.json();
const content = data.choices[0].message.content;
const parsed = JSON.parse(content);
console.log(parsed);
Supported JSON Schema patterns
In practice, the most reliable schemas are small, explicit, and easy for the model to satisfy.
Recommended schema features
typepropertiesrequiredadditionalPropertiesenum- nested objects
- arrays of simple objects
Example with nested object
{
"type": "object",
"properties": {
"customer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" }
},
"required": ["name", "email"],
"additionalProperties": false
},
"status": {
"type": "string",
"enum": ["new", "active", "inactive"]
}
},
"required": ["customer", "status"],
"additionalProperties": false
}
Example with array items
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"quantity": { "type": "number" }
},
"required": ["name", "quantity"],
"additionalProperties": false
}
}
},
"required": ["items"],
"additionalProperties": false
}
Keep schemas practical
Although JSON Schema can be extremely expressive, structured output works best when you:
- keep nesting moderate
- avoid overly complex one-of / all-of logic
- avoid huge schemas unless necessary
- use enums when values are known ahead of time
Common mistakes
1. Asking for JSON in the prompt without using response_format
This is better than nothing, but still weaker than explicit structured output.
Less reliable:
{
"messages": [
{
"role": "user",
"content": "Return JSON with fields name and email"
}
]
}
Better:
{
"messages": [
{
"role": "user",
"content": "Extract the contact information."
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "contact",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" }
},
"required": ["name", "email"],
"additionalProperties": false
}
}
}
}
2. Forgetting additionalProperties: false
If you want strict output, add it explicitly. Otherwise the model may include extra keys.
3. Overcomplicating the schema
Very large or deeply nested schemas increase prompt size and may reduce reliability.
4. Skipping validation in application code
Even with structured output, always validate the parsed result before using it in production systems.
5. Using free-form strings where enums would work better
If a field should only be one of a few values, use enum.
Bad:
{
"priority": { "type": "string" }
}
Better:
{
"priority": {
"type": "string",
"enum": ["low", "medium", "high"]
}
}
Best practices
Prefer json_schema for production
If the output feeds another system, schema-constrained output is usually the safest choice.
Keep field names stable
Use field names you actually want in your codebase. Avoid renaming them later if possible.
Validate after parsing
Use:
- Zod in TypeScript
- Pydantic in Python
- JSON Schema validators in backend services
Keep schemas small and focused
One request should ideally produce one clear object shape.
Combine structured output with tool calling carefully
Structured output and tool calling solve different problems:
| Feature | Best for |
|---|---|
| Structured output | returning typed data |
| Tool calling | invoking application functions |
If you need the model to return data, use structured output.
If you need the model to trigger behavior, use tool calling.
Watch token usage
Large schemas increase prompt_tokens. If a schema is big, it affects both cost and latency.
Structured output vs tool calling
These two features are related, but they are not interchangeable.
Structured output
Use when you want the model to return a JSON object such as:
- extracted invoice fields
- moderation labels
- routing decisions
- article metadata
- summary records
Tool calling
Use when you want the model to request an action such as:
- searching your knowledge base
- querying a live API
- creating a support ticket
- checking account state
- updating a workflow
Example decision guide
| Goal | Use |
|---|---|
| “Return a structured contact record” | structured output |
| “Look up the current weather” | tool calling |
| “Classify this support request into one of five categories” | structured output |
| “Create a task in our CRM” | tool calling |
End-to-end extraction example
Goal
Extract a support ticket payload from unstructured text in a form your backend can accept directly.
Request
{
"model": "openai/gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Create a support ticket from this message: I was billed twice for my plan and need a refund. My email is alex@example.com."
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "support_ticket",
"schema": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "technical", "account"]
},
"email": {
"type": "string"
},
"subject": {
"type": "string"
},
"summary": {
"type": "string"
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high"]
}
},
"required": ["category", "email", "subject", "summary", "priority"],
"additionalProperties": false
}
}
}
}
Possible model output
{
"category": "billing",
"email": "alex@example.com",
"subject": "Duplicate billing and refund request",
"summary": "The customer reports being charged twice for their plan and is requesting a refund.",
"priority": "normal"
}
This can now go directly into your application’s validation and persistence layer.
Operational guidance
When using structured output in production:
- log the raw model content before parsing
- parse JSON in a try/catch block
- validate the parsed object
- fail gracefully if validation fails
- optionally retry with a simpler prompt or schema
TypeScript safe parse pattern
import { z } from "zod";
const TicketSchema = z.object({
category: z.enum(["billing", "technical", "account"]),
email: z.string().email(),
subject: z.string(),
summary: z.string(),
priority: z.enum(["low", "normal", "high"]),
});
function parseStructuredOutput(raw: string) {
try {
const parsedJson = JSON.parse(raw);
return TicketSchema.parse(parsedJson);
} catch (error) {
console.error("Structured output parse failed:", error);
return null;
}
}
Python safe parse pattern
from pydantic import BaseModel, ValidationError
import json
class Ticket(BaseModel):
category: str
email: str
subject: str
summary: str
priority: str
def parse_structured_output(raw: str):
try:
parsed_json = json.loads(raw)
return Ticket.model_validate(parsed_json)
except (json.JSONDecodeError, ValidationError) as exc:
print("Structured output parse failed:", exc)
return None
Next steps
- API Reference — full request and response schema details
- Tool Calling — invoke functions and external systems
- Streaming — handle incremental responses and final usage data
- Vision & Multimodal — combine extraction with image inputs
- Errors — retry strategy, validation failures, and operational debugging