How to Get Structured JSON Output from Any LLM (Reliably)

How to Get Structured JSON Output from Any LLM (Reliably)

JSON mode, function calling, schema constraints, and prompt engineering — the complete toolkit for reliable structured output across GPT-4o, Claude, and Gemini.

Author

AICredits Team

Published

8 Apr 2026

Reading time

7 min read

Why structured output is hard

LLMs are trained to produce human-readable text, not machine-parseable data. Even when you ask for JSON, the model might wrap it in markdown fences, add a preamble, include trailing commas, or hallucinate fields. In production, any of these cause your JSON parser to throw and break your application.

The good news: every major model now has a mechanism for enforcing structured output.

Technique 1: JSON mode

The most reliable approach for OpenAI-compatible APIs. Set response_format to {"type": "json_object"} — the model is constrained to produce only parseable JSON.

import json
from openai import OpenAI
 
client = OpenAI(
    base_url="https://api.aicredits.in/v1",
    api_key="sk-your-aicredits-key",
)
 
response = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    response_format={"type": "json_object"},
    messages=[
        {
            "role": "system",
            "content": (
                "Extract these fields and return valid JSON only: "
                '{"name": string, "email": string, "issue": string}'
            ),
        },
        {
            "role": "user",
            "content": "Hi, I'm Priya ([email protected]) and my invoice shows the wrong amount.",
        },
    ],
)
 
data = json.loads(response.choices[0].message.content)
print(data)
# {'name': 'Priya', 'email': '[email protected]', 'issue': 'Invoice shows wrong amount'}

Technique 2: Function calling / tool use

The strictest approach — define a JSON Schema and the model will fill it exactly. Use this for complex nested schemas with required fields and enum constraints.

tools = [{
    "type": "function",
    "function": {
        "name": "extract_ticket",
        "description": "Extract structured data from a support ticket",
        "parameters": {
            "type": "object",
            "properties": {
                "name":     {"type": "string"},
                "email":    {"type": "string", "format": "email"},
                "issue":    {"type": "string"},
                "priority": {"type": "string", "enum": ["low", "medium", "high"]},
            },
            "required": ["name", "email", "issue", "priority"],
        },
    },
}]
 
response = client.chat.completions.create(
    model="openai/gpt-4o-mini",
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "extract_ticket"}},
    messages=[{"role": "user", "content": "I'm Rahul ([email protected]), payment keeps failing, urgent!"}],
)
 
import json
args = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
print(args)
# {'name': 'Rahul', 'email': '[email protected]', 'issue': 'Payment keeps failing', 'priority': 'high'}

Technique 3: Pydantic validation with retry

Parse the JSON, then validate it with Pydantic. On failure, re-prompt with the error message:

from pydantic import BaseModel, ValidationError
 
class SupportTicket(BaseModel):
    name: str
    email: str
    issue: str
    priority: str  # "low" | "medium" | "high"
 
def extract_ticket(user_message: str) -> SupportTicket:
    for attempt in range(2):
        response = client.chat.completions.create(
            model="openai/gpt-4o-mini",
            response_format={"type": "json_object"},
            messages=[
                {"role": "system", "content": "Return JSON: {name, email, issue, priority (low/medium/high)}."},
                {"role": "user",   "content": user_message},
            ],
        )
        try:
            data = json.loads(response.choices[0].message.content)
            return SupportTicket(**data)
        except (json.JSONDecodeError, ValidationError) as e:
            if attempt == 1:
                raise
            # Re-prompt with error on second attempt
            user_message = f"{user_message}\n\nPrevious output failed: {e}. Fix and return valid JSON."
 
ticket = extract_ticket("I'm Rahul, [email protected], payment failed, urgent!")
print(ticket)

Technique 4: Schema-first prompt engineering

For models without JSON mode, include the target schema directly and use a completion cue:

# End the user message with the opening brace to trigger JSON completion
prompt = (
    "Extract: name, email, issue from this message.\n"
    "Return only valid JSON matching: {\"name\": string, \"email\": string, \"issue\": string}\n\n"
    f"Message: {user_message}\n\nJSON: {{"
)

Choosing the right technique

| Scenario | Best technique | |----------|---------------| | Simple flat schema | JSON mode | | Complex schema with enums | Function calling | | Model without JSON mode | Schema-first prompt | | Production with validation | JSON mode + Pydantic |

Related Articles

Continue in Docs

Need implementation commands and endpoint details? Go to quickstart or API reference.