Fixing "Invalid Schema" After Moving Structured Outputs to a New Provider
9 min read · updated August 11, 2026
The request that worked yesterday now returns HTTP 400 with a message about an invalid schema, and the schema has not changed. It was never valid for the new provider — it was valid for the old one, which is a different thing, and the error message usually names the exact keyword if you know how to read it.
The error string
The message that sends people to a search box looks roughly like this:
{
"error": {
"message": "Invalid schema for response_format 'extract_contact': In context=('properties', 'address'), 'additionalProperties' is required to be supplied and to be false.",
"type": "invalid_request_error",
"param": "response_format",
"code": null
}
}Three parts matter. invalid_request_error tells you this is deterministic: retrying will produce the identical response, so any retry loop wrapped around this call is pure latency. The param field names the request field at fault, which distinguishes a structured-output schema problem from a tool-schema problem — the same class of failure occurs on tool definitions and reports a different param. And the context tuple inside the message is a path into your schema.
Other providers word it differently — a schema-subset validator will typically say something closer to “Invalid JSON payload received. Unknown name ‘additionalProperties’” — but the useful content is the same: a location and a keyword.
Reading the context path
The context tuple is a sequence of keys to walk from the root of your schema. ('properties', 'address') means: go into properties, then into the address entry — that object is the one at fault. A deeper failure gives a longer tuple: ('properties', 'orders', 'items', 'properties', 'shipping') walks an array’s item schema and then one of its properties.
An empty tuple, context=(), means the root object itself. That is the most common one on a first migration, and it is also the one people misread as “the error has no location”.
Walk the path in a REPL rather than by eye. On a schema of any size, reading nested JSON by eye is how you spend forty minutes fixing the wrong object:
import json, functools
schema = json.load(open("schemas/extract_contact.json"))
path = ("properties", "address")
node = functools.reduce(lambda d, k: d[k], path, schema)
print(json.dumps(node, indent=2))
print("additionalProperties:", node.get("additionalProperties", "<absent>"))
print("required:", node.get("required", "<absent>"))
print("property keys:", list(node.get("properties", {}).keys()))The four causes
Validators check in roughly this order, so fix them in this order too — the first failure masks the rest, and a schema with three problems will report them one migration-cycle at a time.
- A missing or wrongly-valued
additionalProperties. Strict validators require it present andfalseon every object, including nested ones and the item schemas of arrays of objects. It is easy to set it on the root and forget the four objects underneath. Conversely, a validator built on an OpenAPI subset may reject the keyword entirely, in which case the fix is to strip it rather than add it — the direction depends on which way you are migrating. - A property missing from
required. Under strict mode every key inpropertiesmust also appear inrequired. A schema written for a provider with ordinary optionality semantics will have a shortrequiredarray and fail here. The fix is to list every key and change genuinely optional ones to a nullable type union, then update the code that consumed absent keys to handle explicit nulls instead. - An unsupported keyword. Value constraints —
minLength,maxLength,pattern,minimum,maximum,multipleOf,minItems— and someformatvalues are rejected by validators that do not implement them. The message names the keyword. Strip it from the schema and enforce the constraint in your own validation after parsing. - Recursion or a depth breach. A definition that references itself, or nesting past the provider’s maximum depth, is reported as a structural failure rather than a keyword one, often without a useful context path. Flatten to a fixed depth. If your data really is a tree, return it as a flat list of nodes with parent ids and rebuild the tree yourself.
A fifth cause is worth mentioning because it produces the same error from a completely different place: a schema that is fine but is being sent under the wrong request field. Structured output and tool parameters take schemas in different places and under different key names on every provider — input_schema versus parameters, a schema nested under a named wrapper versus a bare one. Check param in the error before you start editing the schema at all.
Bisecting a large schema
When the context path is unhelpful, or when a generated schema has dozens of objects, do not read it. Bisect it with a script that sends progressively smaller schemas and reports the first that passes.
- Send the schema reduced to nothing but its root object with a single string property. If that fails, the problem is at the root or in your request shape, not in the schema body.
- Add back the top-level properties one at a time, sending each time. The first addition that fails identifies the subtree.
- Recurse into that subtree the same way. Three or four rounds locates any keyword in a schema of realistic size.
- Once found, fix the keyword and re-run the full schema. Expect a second, different failure: validators report one problem at a time and a schema written against different rules usually breaks several.
A useful shortcut before bisecting: run the schema through a transformer that mechanically applies the strict-mode rules — adding additionalProperties: false to every object, moving every property key into required, and stripping the known unsupported keywords — and send the result. If it passes, the diff between the two documents is your list of problems, obtained in one request instead of ten.
Stopping it recurring
This failure is entirely preventable in CI, and the test is cheap because schema validation happens before any generation: a request with an output cap of a handful of tokens and a one-word prompt costs almost nothing and exercises the validator fully. Write a test that sends every schema in your codebase to every provider you might route to, and asserts no 400. It will catch the next occurrence the day the schema changes rather than the day you migrate.
The deeper fix is not to maintain per-provider schema documents at all, but to keep one canonical schema and emit dialects — the approach described in migrating structured output schemas between providers. A transformer you can unit-test is more reliable than a directory of near-identical JSON files that drift.