Troubleshooting

Structured Outputs 400 Errors: Fix strict JSON Schema Before Retrying

A schema-related HTTP 400 is a request problem to inspect before retrying. First read the error's type, message and field reference: a 400 can have causes other than the schema. Retrying an unchanged invalid definition does not repair it.

This guide separates definition checks from output handling and uses only synthetic local examples. It does not establish strict structured-output support for every gateway or model.

Identify the stage that failed

Observation Inspection point
The endpoint rejects the request and names a schema field Inspect the definition and endpoint-specific wrapper
The response indicates a refusal Follow the documented refusal path before parsing expected data
A response is incomplete Inspect completion status and the endpoint's limit information
Output parses as JSON but has an unexpected shape Distinguish JSON mode from schema-constrained output; validate application data

The OpenAI Structured Outputs guide distinguishes JSON mode from schema adherence and documents separate refusal and incomplete-response handling. These cases should not share a blind retry branch.

Use the wrapper for the endpoint

In OpenAI's documented Chat Completions format, the definition goes under response_format.json_schema:

{"response_format":{"type":"json_schema","json_schema":{"name":"triage_result","strict":true,"schema":{"type":"object","properties":{"label":{"type":"string"}},"required":["label"],"additionalProperties":false}}}}

For the documented Responses format, it goes under text.format:

{"text":{"format":{"type":"json_schema","name":"triage_result","strict":true,"schema":{"type":"object","properties":{"label":{"type":"string"}},"required":["label"],"additionalProperties":false}}}}

These are format fragments, not full requests. Consult the current Structured Outputs guide for Responses and the Chat Completions request reference for endpoint-specific fields. Do not mix the two wrappers or assume that every compatible endpoint supports them. Tool parameter schemas are another context: choose the mechanism that matches your application rather than moving a response format into a tool definition.

Start with a small inline definition

OpenAI's strict-format guide requires defined fields to appear in required, documents nullable values for optional semantics, and requires additionalProperties: false for objects. Its supported subset also restricts root shape. Apply those rules to nested definitions, not just the outer object.

Our synthetic result has a required label, a note that can be null, and one nested object:

{
  "type":"object",
  "properties":{
    "label":{"type":"string"},
    "note":{"type":["string","null"]},
    "details":{
      "type":"object",
      "properties":{"verified":{"type":"boolean"}},
      "required":["verified"],
      "additionalProperties":false
    }
  },
  "required":["label","note","details"],
  "additionalProperties":false
}

note is still present in this definition's required list; its value may be null. A client that treats missing and null as interchangeable should make that application decision explicit.

A recursive offline common-error check

The following checker is deliberately limited to inline definitions. It flags references and unhandled combinations for separate review. It is not a complete JSON Schema validator, an OpenAI certification tool or a check of model behavior.

function checkCommonStrictSchema(schema) {
  const errors = [], review = [], seen = new WeakSet();
  function visit(node, at, root = false) {
    if (!node || typeof node !== "object" || Array.isArray(node)) {
      errors.push(at + ": schema node must be an object"); return;
    }
    if (seen.has(node)) { review.push(at + ": reused or cyclic JS object"); return; }
    seen.add(node);
    if (node.$ref) { review.push(at + ": resolve $ref separately"); return; }
    if (root && node.type !== "object") errors.push(at + ": check root object type");
    if (root && node.anyOf) errors.push(at + ": root anyOf needs correction");
    const types = Array.isArray(node.type) ? node.type : [node.type];
    if (types.includes("object")) {
      const props = node.properties || {}, names = Object.keys(props);
      if (node.additionalProperties !== false) errors.push(at + ": additionalProperties must be false");
      if (!Array.isArray(node.required) || names.some(k => !node.required.includes(k)) ||
          node.required.some(k => !Object.hasOwn(props, k))) errors.push(at + ": check required fields");
      for (const [name, child] of Object.entries(props)) visit(child, at + ".properties." + name);
    }
    if (types.includes("array")) {
      if (!node.items) errors.push(at + ": review missing items");
      else visit(node.items, at + ".items");
    }
    if (node.anyOf !== undefined && !Array.isArray(node.anyOf)) errors.push(at + ": anyOf must be an array");
    else for (const [i, branch] of (node.anyOf || []).entries()) visit(branch, at + ".anyOf[" + i + "]");
    for (const k of ["oneOf","allOf","not","if","then","else"]) {
      if (k in node) review.push(at + ": review " + k + " support");
    }
  }
  visit(schema, "$", true);
  return {errors, review, commonChecksPassed: !errors.length && !review.length};
}

Save the function in check-schema.cjs, add your synthetic definition as schema, and print checkCommonStrictSchema(schema). Node runs this without an API connection. Use a standards-aware validator for reference resolution and instance validation, then consult the endpoint's current supported subset.

Reproduce a failure without a model request

Starting from the inline fixture:

  1. Remove the nested object's additionalProperties. The checker identifies the nested path.
  2. Remove note from required. It identifies a required-field mismatch.
  3. Restore both to clear those common errors.
  4. Replace the root with an anyOf union. Review the documented root restriction.
  5. Use a $ref definition. The checker asks for separate resolution instead of certifying it.

Keep this as a local regression fixture when your application generates schemas. It can catch an accidental definition change before a request leaves your machine. A passing result still does not establish the provider's entire keyword support, a valid full API body or a successful model response.

Inspect the actual serialized request

Generated definitions can differ from the object you expected. Compare the final wrapper and schema produced by your serializer using a synthetic reproduction. Separate local JSON syntax errors, definition constraints and model-output parsing failures.

When reporting a problem, retain the endpoint family, relevant field path and redacted error, plus a minimal synthetic definition. Do not publish a real API credential, private input or customer output. Reduce the definition to the smallest case that preserves the issue before adding fields back.

Choose the next step without assuming support

Use the OpenAI-compatible SDK guide, HTTP examples and error guide for your selected route. The API setup tool checks address format; it does not certify structured-output capabilities.

Confirm the chosen model and endpoint's supported features separately. If strict schema output is unavailable, decide explicitly how your application will parse and validate a different output mode rather than silently treating it as equivalent. This article offers no price, availability or reliability guarantee.

分享这篇文章

开始使用 LLM API

免费套餐可用。Claude Code 一行配置。

免费开始