Manual response_format
On this page
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:
- The API returns the JSON as a string in
response.choices[0].message.content - 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()
| Aspect | parse() | response_format |
|---|---|---|
| Return | (Response, ParsedModel) | Response only |
| Parsing | Automatic | Manual |
| Data access | parsed.field | json.loads() or model_validate_json() |
| Flexibility | Standard | Maximum |
| Error handling | Built-in | Your 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_formatreturns a JSON string inresponse.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