Strict Mode in OpenAI Function Calling: What It Rejects
9 min read · updated August 11, 2026
strict: true changes function calling from “the model usually produces arguments matching your schema” to “the arguments match your schema”. The price is that your schema must be inside a subset of JSON Schema, and schemas that were fine before now fail at request time with a 400.
What strict mode guarantees
Without strict mode, the schema in a tool definition is a description the model is prompted with. It is usually followed. It is not enforced, which is why production code has to validate function.arguments after parsing it, and why a required field being absent is a real failure mode rather than a theoretical one.
With strict: true, OpenAI constrains decoding so that only tokens which can continue a valid instance of your schema are sampleable. The generated arguments conform by construction. OpenAI describes the mechanism and the supported subset in its Structured Outputs guide; the same machinery backs response_format with a json_schema, covered in response_format with a JSON schema.
The reason there is a subset at all follows from the mechanism. To constrain decoding, the schema is compiled into something that can answer “which tokens are legal next” at every position. Keywords that can only be checked once a value is complete — a numeric range, a regular expression over a whole string, a disjunction requiring backtracking — do not fit that model cheaply, and the ones that do not fit are excluded.
The three structural rules
- Every object must set
"additionalProperties": false. Not just the root — every nested object, every object inside an array’sitems. Omitting it anywhere is the single most common cause of a rejected strict schema. - Every property must be listed in
required. All of them. There is no such thing as an optional property in a strict schema; optionality is expressed differently, and that is the next section. - The root must be an object. Not an array, not a string, not a union. If what you want is a list, wrap it:
{"items": [...]}.
Beyond those, the supported type vocabulary is the ordinary one — string, number, integer, boolean, object, array, null, plus enum and anyOf. $ref and $defs work, including for recursive schemas, which is how you express a tree.
Three schemas it rejects
1. A nested object without additionalProperties
{
"name": "create_invoice",
"strict": true,
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["customer", "total_cents"],
"properties": {
"total_cents": {"type": "integer"},
"customer": {
"type": "object", // ← no additionalProperties here
"required": ["name"],
"properties": {"name": {"type": "string"}}
}
}
}
}The rejection names the path:
HTTP/1.1 400 Bad Request
{
"error": {
"message": "Invalid schema for function 'create_invoice':
In context=('properties', 'customer'),
'additionalProperties' is required to be supplied
and to be false.",
"type": "invalid_request_error",
"param": "tools[0].function.parameters",
"code": "invalid_function_parameters"
}
}The context tuple is the useful part: it is the path to the offending subschema. On a schema with forty nested objects that is the difference between a fix and an afternoon.
2. A property left out of required
{
"type": "object",
"additionalProperties": false,
"required": ["name"], // ← "nickname" is missing
"properties": {
"name": {"type": "string"},
"nickname": {"type": "string"}
}
}Rejected with a message to the effect that required must contain every key in properties. This one trips people because it is valid JSON Schema and expresses a perfectly ordinary intent — the nickname is optional. Strict mode does not accept that spelling of it.
3. Constraint keywords outside the subset
{
"type": "object",
"additionalProperties": false,
"required": ["email", "age", "shape"],
"properties": {
"email": {"type": "string", "format": "email", "pattern": "^.+@.+$"},
"age": {"type": "integer", "minimum": 0, "maximum": 120},
"shape": {"oneOf": [{"type": "string"}, {"type": "number"}]}
}
}Three separate problems in one object: validation keywords that check a completed value rather than a prefix, and oneOf, which requires exactly-one-match semantics that constrained decoding does not implement. anyOf is the supported disjunction and is usually what was meant.
How to express an optional field
Since every property must be required, optionality is expressed as a value that may be null:
{
"type": "object",
"additionalProperties": false,
"required": ["name", "nickname"],
"properties": {
"name": {"type": "string"},
"nickname": {"type": ["string", "null"]}
}
}The model must now emit the key, and it may emit null for it. That is a genuine improvement over the optional-property version and not merely a workaround: “absent” and “the model forgot” are indistinguishable in the optional spelling, whereas an explicit null is a decision the model made. Your deserialiser maps null to whatever absence means in your language.
The same trick handles conditional shapes. A tool that takes either a user id or an email becomes an object with both keys, both nullable, and a description saying exactly one must be non-null. The schema cannot enforce that; the description gets you most of the way, and your handler enforces the rest.
Size limits and what strict does not cover
Strict schemas have documented ceilings on total properties, nesting depth, total enum values and total string length across the schema. These have been raised at least once since the feature launched, so the only correct move is to read the current figures from the Structured Outputs guide. The shape of the constraint is what to design against: a schema with hundreds of enum values or a dozen levels of nesting is near the edge of what is supported, and is usually near the edge of what a model handles well anyway.
Four things strict mode does not give you:
- Correct values. A conforming string is not a true string. The model can produce a well-typed, well-shaped, entirely wrong invoice.
- Completion. Run out of output tokens mid-object and you get a valid prefix of a conforming object, which does not parse. The finish reason still has to be checked — see the output token cap.
- An answer. The model may refuse, in which case the message carries a
refusalfield instead of the tool call, and that field is not schema-constrained. - A free first call. The first request with a new strict schema pays a latency cost while the schema is processed; subsequent requests with the identical schema do not. Generating schemas dynamically per request forfeits that.