Format output_schema root must be an object: wrap an array root

A Sume Format run rejects an output_schema whose root is an array or type union with 400 and rule root_must_be_object. Wrap it in a named key.

5 min readSume
All posts

A Sume Format run needs the root of output_schema to be a single {"type": "object"}. A root array, a root string and even {"type": ["object", "null"]} are all refused at create with 400 output_schema_invalid, and the violation carries the stable rule token root_must_be_object. The fix is to wrap the thing you wanted at the root in a named key, such as captions.

The rule comes from the Structured output page, which says schemas must satisfy the OpenAI strict-mode subset. OpenAI's own page, Structured model outputs, read on 2026-10-03, also says the root must be an object, so a schema written for that API usually passes this check. A schema exported from a typed list model is the common way to hit it.

What does the 400 look like?

Nothing runs and nothing is charged, because the schema is checked before the run exists. Every problem is reported in one response in details.violations[], each entry a { path, rule, message }. path is a JSON-Pointer-style location, message is for people and may change, and rule is the field to switch on.

For a root-level problem the rule is root_must_be_object. The docs define it as the root being missing, not an object, or having a type that is not exactly "object". That last clause is why a nullable root is refused: the type must be one single string, not a list.

Which root shapes are refused?

The table lists what the documentation says about each, with the fix.

Root shapes and their outcome, from Sume's Structured output page (read 2026-10-03)
Root you sentOutcomeFix
{"type": "array", "items": {...}}Rejected, root_must_be_objectWrap: an object with one array property
{"type": "string"}Rejected, root_must_be_objectWrap in an object with one string property
{"type": ["object", "null"]}Rejected: the type must be exactly objectUse object; express absence on fields with a null union
{"type": "object"} with no additionalProperties: falseRejected, additional_properties_falseAdd additionalProperties: false and list every property in required

How do I wrap an array root?

Keep the array and give it a name. The docs' own example turns a list of strings into an object with a captions array. Every object still needs additionalProperties: false, and every declared property must be in required, so the wrapper lists captions there.

Your code then reads run.output.captions instead of run.output. If you want an empty list when the run has nothing to say, leave minItems off the array, because the docs say minItems is genuinely enforced.

{
  "output_schema": {
    "name": "acme/captions/v1",
    "strict": true,
    "schema": {
      "type": "object",
      "additionalProperties": false,
      "required": ["captions"],
      "properties": {
        "captions": { "type": "array", "items": { "type": "string" } }
      }
    }
  }
}

What does this not fix?

Wrapping the root only satisfies the shape rules. Sume still builds output from what the run made and said, and the URL gate checks every URL in it against media this run produced, so a schema that demands a file the Format never makes can still end in output_schema_unsatisfied after the run. That is a run-time outcome, covered on the Errors and spend page, and it is different from the submit-time 400 here.

Sume also does not offer a loose "any JSON" mode for the root. You bind a schema, or you take the built-in sume/action-run-output/v1 shape by sending none.

Why wrap instead of asking for a bare list?

A named key is also the more stable contract. An object root can gain a second field later, such as notes or language, without changing the type of the whole response, while a bare array can only ever be a list. Because Sume requires every declared property to be listed in required, adding the field later means a new schema name and a deliberate change on your side.

Name the schema too. The envelope is { name, strict, schema }, and the receipt echoes name, strict and source back under output_schema, where source is request_override when you sent one. Putting a version in the name, as in acme/captions/v1, makes it obvious in a stored receipt which contract a run was held to.

If you are porting from another provider, check the root before anything else. A typed list model in a client library often serialises to a root array, and that is the first violation you will see.

Create the run with the wrapped schema. The schema is validated at create, so a root problem comes back as a 400 before any run exists, and a 400 at create means nothing ran and nothing was charged. If you get a 202, the shape rules have passed and the receipt's output_schema.source should read request_override.

Then keep the wrapped key in your reader. In TypeScript that is run.output.captions, and in Python run["output"]["captions"]. Check output_error first, because the docs say to check it before reading output.

Sources

Related posts

More in Formats

All Formats posts

Written by Sume