Skip to content

transform_schema raises AssertionError on valid list-form types, e.g. {"type": ["string", "null"]} #1764

Description

@shamashel

Summary

transform_schema (the structured-output schema transformer used by messages.parse / beta.messages.parse) crashes with an AssertionError on any schema that uses the list form of type — e.g. {"type": ["string", "null"]} — which is valid JSON Schema and explicitly supported by the Anthropic structured-outputs API.

The failure happens at request-build time, before any API call.

Repro

from anthropic import transform_schema

# Valid JSON Schema; also documented as supported by the structured-outputs API
transform_schema({"type": ["string", "null"]})

One-liner:

python -c 'from anthropic import transform_schema; transform_schema({"type": ["string", "null"]})'

Actual (on anthropic==0.112.0; the relevant code is unchanged on current main of src/anthropic/lib/_parse/_transform.py):

AssertionError: Expected code to be unreachable, but got: ['string', 'null']

A more realistic nested variant fails the same way:

transform_schema({
    "type": "object",
    "properties": {"name": {"type": ["string", "null"]}},
    "required": ["name"],
    "additionalProperties": False,
})

Expected

An equivalent transformed schema, e.g. rewriting the type union into anyOf branches:

{"anyOf": [{"type": "string"}, {"type": "null"}]}

Why this should be supported

  • JSON Schema allows type to be "a string or an array of strings" — the list form is standard, not an extension.
  • The structured outputs docs explicitly document type arrays as supported (and meter them): "Total parameters that use anyOf or type arrays (for example, "type": ["string", "null"]) across all strict schemas." So the API accepts this shape; only the SDK-side transformer rejects it.
  • Nullable-type unions are pervasive in real schemas: Pydantic Optional[...] fields in older/alternative generators, OpenAPI 3.1 output, GraphQL-derived schemas, and MCP tool inputSchemas commonly emit "type": [..., "null"].

Root cause

In src/anthropic/lib/_parse/_transform.py, type_ is typed and handled as a scalar:

type_: Optional[SupportedTypes] = json_schema.pop("type", None)
...
elif type_ == "boolean" or type_ == "integer" or type_ == "number" or type_ == "null" or type_ is None:
    pass
else:
    assert_never(type_)

A list value matches none of the scalar branches and falls into assert_never.

Suggested fix

When type_ is a list, canonicalize it before the scalar handling, e.g. rewrite {"type": [t1, t2, ...], **rest} into an anyOf of single-type branches (with sibling constraints attached to the non-null branches) and recurse. Single-element lists can collapse to the scalar form.

Related (not duplicates)

Environment

  • anthropic==0.112.0 (also reproduces against main's _transform.py)
  • Python 3.12.13, macOS arm64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions