Skip to content
Owais KhanSoftware Reviews
Free browser tool

OpenAI Structured Outputs JSON Schema Converter

Paste a JSON Schema, get one the OpenAI API accepts with strict: true.

Structured Outputs will not accept an ordinary JSON Schema. Every object in it — not just the root, but the ones inside array items, $defs and anyOfbranches — needs additionalProperties: false and a required array naming every one of its properties. Strict mode also has no concept of an optional field, so anything you left out of required has to be re-expressed as a nullable union, and a set of JSON Schema keywords is rejected outright. Miss any one of these in any nested object and the API answers with a 400 that points at a single path, which is why fixing a large schema by hand turns into a loop of paste, call, read error, repeat.

This converter applies every rule recursively in one pass, shows you a diff of exactly what it changed and why, and hands back runnable Python and TypeScript. It is the same tool whether you searched for an OpenAI strict JSON schema converter, wanted toconvert a JSON Schema to OpenAI strict mode, needed anOpenAI structured output schema generator, or just wanted anadditionalProperties false JSON schema converter.

Your data never leaves your browser. All processing happens locally. Nothing you paste is uploaded, logged or stored, and the converter issues no network request of any kind — disconnect the network and it still works.

Convert a JSON Schema to OpenAI strict mode

Paste the schema you are sending to the API. It is converted as you type.

Options

Strict mode has no optional fields. Nullable is how OpenAI's docs emulate one.

Also strips minLength, maxLength, pattern, format, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minItems, maxItems, which fine-tuned models reject.

{
  "type": "object",
  "properties": {
    "invoice_id": {
      "type": "string"
    },
    "customer": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "email": {
          "type": [
            "string",
            "null"
          ],
          "format": "email"
        }
      },
      "required": [
        "name",
        "email"
      ],
      "additionalProperties": false
    },
    "line_items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "properties": {
          "sku": {
            "type": [
              "string",
              "null"
            ]
          },
          "qty": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1
          }
        },
        "required": [
          "sku",
          "qty"
        ],
        "additionalProperties": false
      }
    },
    "notes": {
      "type": [
        "string",
        "null"
      ],
      "maxLength": 500
    },
    "status": {
      "type": [
        "string",
        "null"
      ],
      "enum": [
        "draft",
        "sent",
        "paid",
        null
      ]
    }
  },
  "required": [
    "invoice_id",
    "customer",
    "line_items",
    "notes",
    "status"
  ],
  "additionalProperties": false
}

What changed (11)

  • #/properties/customer/properties/email — Was optional. Strict mode has no optional fields, so its type was unioned with `"null"`.
  • #/properties/customer — Added `email` to `required`.
  • #/properties/customer — Added `"additionalProperties": false` — required on every object.
  • #/properties/line_items/items/properties/sku — Was optional. Strict mode has no optional fields, so its type was unioned with `"null"`.
  • #/properties/line_items/items/properties/qty — Was optional. Strict mode has no optional fields, so its type was unioned with `"null"`.
  • #/properties/line_items/items — Added `required` listing all 2 properties — strict mode requires every property to be listed.
  • #/properties/line_items/items — Added `"additionalProperties": false` — required on every object.
  • #/properties/notes — Was optional. Strict mode has no optional fields, so its type was unioned with `"null"`.
  • #/properties/status — Was optional. Strict mode has no optional fields, so its type was unioned with `"null"`.
  • # — Added `notes`, `status` to `required`.
  • # — Set `"additionalProperties": false` — it must be false on every object.

How it works

The converter parses your schema and walks every node once, rebuilding it in place so the key order you wrote is preserved and the diff stays readable. At each node it applies the rules from OpenAI's Structured Outputs documentation:

  1. Every object gets additionalProperties: false. Including objects reached through items, anyOf and $defs — the ones a manual fix usually misses.
  2. required lists every property. Not just the ones you marked; strict mode permits no exceptions.
  3. Anything that was optional becomes nullable. "string" becomes["string", "null"], an anyOf gains a null branch, and an optional $ref is wrapped in anyOf because a $refcannot carry a sibling type.
  4. Unsupported keywords are removed and oneOf is rewritten.oneOf becomes anyOf, which keeps your branches instead of discarding them.
  5. Draft-07 definitions is renamed to $defs, and every $ref pointing into it is rewritten so nothing dangles.
  6. The documented size limits are checked. These cannot be auto-fixed, so they are reported rather than silently ignored.

Which JSON Schema keywords are unsupported by OpenAI Structured Outputs

This is the table most write-ups get wrong, because OpenAI moved a whole group of keywords from "not yet supported" to supported during 2025. String patterns and numeric bounds now work on base models.

GroupKeywordsStatus
Supported typesstring, number, integer, boolean, object, array, enum, anyOf, $ref / $defsSupportedRecursion via $ref: "#" is allowed.
String / number / array constraintsminLength, maxLength, pattern, format, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minItems, maxItemsSupportedSupported on base models. Not supported on fine-tuned models.
CompositionallOf, not, dependentRequired, dependentSchemas, if, then, else, oneOfNot supportedRejected. oneOf is rewritten to anyOf; the rest are removed.
Other object / array keywordspatternProperties, unevaluatedProperties, propertyNames, minProperties, maxProperties, unevaluatedItems, prefixItems, additionalItems, contains, minContains, maxContains, uniqueItems, default, examples, readOnly, writeOnly, deprecated, $schema, $id, $commentNot supportedAbsent from the supported list — removed.

Size limits a strict schema must stay inside

These are not fixable by rewriting keywords — if a schema exceeds one, it has to be simplified. The converter reports them so you find out here rather than from a 400.

Total object properties5,000
Object nesting depth10 levels
Total string length of property names, enum and const values120,000 characters
Enum values across the whole schema1,000
A single enum with more than 250 values15,000 characters total

Optional fields in OpenAI strict mode

The single most common cause of a strict-mode 400 afteradditionalProperties is assuming required is optional. It is not: a property that exists in properties but not in required is invalid, full stop. The fix is not to delete the property — it is to require it and let its value be null. In Pydantic that is Optional[str]with no default; adding = None makes the field optional in the emitted schema and puts you straight back to the same error. The generated Python on this page gets that right.

Where the rules come from

Every rule and limit above is taken from OpenAI'sStructured Outputs documentation. If OpenAI moves another keyword into the supported set, this tool is wrong until its tables are updated — so check the source if something here contradicts a fresh 400.

More utilities: all free developer tools.

Frequently asked questions

Why does OpenAI Structured Output require additionalProperties: false on all objects?
Strict mode guarantees the model returns JSON matching your schema exactly, and it does that by compiling the schema into a constrained decoder rather than by validating the output afterwards. That decoder has to know the complete, closed set of keys it is allowed to emit at every point. An object that permits additional properties has no such closed set, so there is nothing to constrain — the guarantee would be unenforceable. OpenAI therefore rejects the schema up front with a 400 rather than accepting it and silently dropping the guarantee. The requirement applies to every object in the schema, including ones nested inside array items, $defs and anyOf branches, which is the part people miss: adding it only at the root still returns the same error.
How does this tool handle optional fields in OpenAI strict mode?
Strict mode has no optional fields — every property must be listed in required. The documented way to express "this may be absent" is a union with null, so by default this converter adds the missing property names to required and changes their type from "string" to ["string", "null"]. Special cases are handled too: an optional enum also gets null added to its value list (otherwise the type permits null but no listed value can satisfy it), and an optional $ref is wrapped in anyOf with a null branch, because a $ref cannot carry a sibling type. If you would rather assert the model must always produce a value, switch the "Optional fields" setting to "Mark required" and no nullable unions are added.
Which JSON Schema keywords are unsupported by OpenAI Structured Outputs?
The composition keywords are rejected outright: allOf, not, dependentRequired, dependentSchemas, if, then, else. oneOf is not on the supported list either, but anyOf is, so this tool rewrites oneOf as anyOf instead of deleting your branches. Keywords absent from the supported list — such as patternProperties, unevaluatedProperties, propertyNames, minProperties, maxProperties — are stripped as well. Note that a group of keywords people still assume is banned is now supported on base models: minLength, maxLength, pattern, format, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minItems, maxItems. This converter keeps those. They remain unsupported on fine-tuned models, so tick "Target a fine-tuned model" if that is what you are calling and they will be removed.
Is my JSON schema uploaded or processed on a server?
No. The conversion runs entirely in your browser using JavaScript served with the page. Your schema is never uploaded, transmitted, logged or stored — this site has no backend to send it to, and the converter itself issues no network request of any kind. You can confirm it in your browser devtools: open the Network tab and convert a schema, and you will see no request carrying your input. For completeness, the one request you may see is a Cloudflare analytics beacon to /cdn-cgi/rum on this same domain, added by the host to report anonymous page-load timings. It fires on page load, never reads the form, and never sees your schema. The tool also works with the network disconnected once the page has loaded, which is the simplest way to prove the point to yourself.