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:

ModeDescriptionBest for
json_objectForces the model to return a valid JSON objectsimple extraction, lightweight automation
json_schemaConstrains output to a JSON Schemaproduction 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

  • type
  • properties
  • required
  • additionalProperties
  • enum
  • 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:

FeatureBest for
Structured outputreturning typed data
Tool callinginvoking 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

GoalUse
“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