OpenAI with strict JSON Schema outputs
Table of Contents
There are many tutorials1 instructing you to use .description in your JSON Schema for structured outputs. But no one is telling this field is ignored. The official doc doesn't includes it in the "supported fields" but uses it all the time, implicitly telling it works.
Let's Take Conclusions
The test I'll be doing is simple:
- Request a instruction at prompt and a divergent instruction at field description
- I expect it to follow the property description.
I'll be using restclient.el because its syntax is a explicit and readable HTTP request. No magic behind.
Picking .choices.[0].message.content of the following request2, with strict schema:
:token := (+pass-get-secret "net/openai.com/token/blog")
:system = Start message with 'Hello'.
:schema-description = This MUST start with 'Hey'.
:user = Say hi
POST https://api.openai.com/v1/chat/completions
Content-Type: application/json
Authorization: Bearer :token
{
"model": "gpt-4.1",
"messages": [
{"role": "system", "content": ":system"},
{"role": "user", "content": ":user"}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "messageSchema",
"strict": true,
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["message"],
"properties": {
"message": {
"type": "string",
"description": ":schema-description"
}
}
}
}
},
"temperature": 0.6
}
"{\"message\":\"Hey! How can I assist you today?\"}"
OpenAI Python SDK Try
It's , and I'm not satisfied with these results, so I'll reproduce exactly what I did in my previous implementation.
For it, we'll need to setup deps, I'll be using uv:
[project] name = "openai-json-schema" version = "0.0.1" requires-python = ">=3.13"
And run:
uv venv --allow-existing uv add openai pydantic
Minimal code trying to reproduce the above raw request:
from json import dumps from openai import OpenAI from openai.lib._pydantic import to_strict_json_schema from openai.types.chat import ( ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam, ) from pydantic import BaseModel, TypeAdapter class ResponseModel(BaseModel): "This MUST start with 'Hey'." message: str client = OpenAI(api_key=openai_api_key) system_message: ChatCompletionSystemMessageParam = { "role": "system", "content": "Start message with 'Hello'.", } user_message: ChatCompletionUserMessageParam = {"role": "user", "content": "Say hi"} completion = client.chat.completions.parse( model="gpt-4.1", messages=[system_message, user_message], temperature=0.6, response_format=ResponseModel, ) choice_message = completion.choices[0].message.parsed # Completion result print(choice_message.model_dump_json(indent=2)) # Printing how OpenAI sends the JSON Schema (yes, the SDK changes the schema to make it more strict) print(dumps(to_strict_json_schema(TypeAdapter(ResponseModel)), ensure_ascii=False, indent=2))
{
"message": "Hey, hello! How can I help you today?"
}
{
"description": "This MUST start with 'Hey'.",
"properties": {
"message": {
"title": "Message",
"type": "string"
}
},
"required": [
"message"
],
"title": "ResponseModel",
"type": "object",
"additionalProperties": false
}
As I can see, there's a difference in how I provided the description: It wasn't for the field, but for the model.
But either way, it also worked now.
I'll be researching more.
Conclusion
OK, I was wrong, it works. I will review the implementation that misled me into this conclusion.
In the end, I hope it can serve as example for how call the completions API it without SDKs.