Skip to main content
Lesson 5 of 12 · Structured JSON Outputs

Manual response_format

Lesson 5 of 12 Updated July 30, 2026 Developer
On this page
  1. More control with response_format
  2. Workflow
  3. Complete example
  4. Differences from parse()
  5. Parsing without Pydantic
  6. When to choose response_format
  7. Error handling
  8. Key takeaways

More control with response_format

The manual method via response_format gives you full control over parsing. Unlike parse(), which returns a typed object, this approach returns a JSON string that you parse yourself—the Pydantic model only provides the schema (model_json_schema()) and validates afterward. Choose this method when you need flexibility or when working in an environment without Pydantic.

Workflow

With response_format, the process happens in two steps:

  1. The API returns the JSON as a string in response.choices[0].message.content
  2. You parse this string using your method of choice

Complete example

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI(
    api_key="your-xai-key",
    base_url="https://api.x.ai/v1"
)

class MovieReview(BaseModel):
    title: str
    rating: float
    summary: str
    recommend: bool

response = client.chat.completions.create(
    model="grok-4.20-0309-reasoning",
    messages=[{
        "role": "user",
        "content": "Review the movie Inception"
    }],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "movie_review",
            "schema": MovieReview.model_json_schema()
        }
    }
)

# The content is a JSON string
json_string = response.choices[0].message.content
print(type(json_string))  # <class 'str'>
print(json_string)
# {"title": "Inception", "rating": 9.2, "summary": "...", "recommend": true}

# Manual parsing with Pydantic
parsed = MovieReview.model_validate_json(json_string)
print(parsed.title)  # "Inception"

Differences from parse()

Aspectparse()response_format
Return(Response, ParsedModel)Response only
ParsingAutomaticManual
Data accessparsed.fieldjson.loads() or model_validate_json()
FlexibilityStandardMaximum
Error handlingBuilt-inYour responsibility

Parsing without Pydantic

You don’t have to use Pydantic to parse the result. The standard json module works too:

import json

response = client.chat.completions.create(
    model="grok-4.20-0309-reasoning",
    messages=[{
        "role": "user",
        "content": "Analyze this text and extract entities"
    }],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "entities",
            "schema": {
                "type": "object",
                "properties": {
                    "people": {
                        "type": "array",
                        "items": { "type": "string" }
                    },
                    "places": {
                        "type": "array",
                        "items": { "type": "string" }
                    }
                }
            }
        }
    }
)

data = json.loads(response.choices[0].message.content)
print(data["people"])  # ["Marie Curie", "Pierre Curie"]
print(data["places"])      # ["Paris", "Warsaw"]

When to choose response_format

response_format shines whenever you need to see or inspect the JSON before it becomes an object. The most common case is logging: in production, keeping the raw JSON before transformation is often an audit or debugging requirement, and parse() doesn’t give you that interception point. Next comes the schema question: if your interface contract is a raw JSON schema—shared with other teams, generated by a tool, versioned independently of the Python code—enforcing it directly is more faithful than reconstructing it as a Pydantic model. Finally, not every project operates within the Pydantic ecosystem: if your validation goes through another framework, or through an intermediate processing step specific to your pipeline, response_format hands you the JSON as is and leaves you in control of what comes next.

Error handling

With the manual method, you must handle parsing errors yourself:

import json

try:
    data = json.loads(response.choices[0].message.content)
except json.JSONDecodeError as e:
    print(f"Parsing error: {e}")

In practice, this error should never occur since the API guarantees valid JSON. But defensive handling remains a best practice in production.

Key takeaways

  • response_format returns a JSON string in response.choices[0].message.content
  • Parsing is your responsibility: json.loads(), model_validate_json(), or another method
  • You can use a raw JSON schema instead of a Pydantic model
  • More flexibility than parse(), but more code to write
  • The returned JSON is guaranteed valid—parsing errors should not occur