From 194988782ee103ae982f429dc4d3fd6434a057c7 Mon Sep 17 00:00:00 2001 From: mukktinaadh <159904553+mukktinaadh@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:38:25 +0530 Subject: [PATCH] fix(lib): normalise JSON Schema type arrays instead of hitting assert_never (Fixes #1877) - Convert type arrays (e.g., ["string", "null"]) to anyOf schemas - Add _normalize_type_array helper to validate and convert type arrays - Handle type arrays before anyOf/oneOf/allOf processing - Add validation for type arrays (empty, duplicates, unsupported types) - Includes comprehensive tests for type array handling Fixes #1877 --- .inline-snapshot/files_using_external.txt | 4 ++ src/anthropic/lib/_parse/_transform.py | 72 +++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .inline-snapshot/files_using_external.txt diff --git a/.inline-snapshot/files_using_external.txt b/.inline-snapshot/files_using_external.txt new file mode 100644 index 000000000..b68c8b34f --- /dev/null +++ b/.inline-snapshot/files_using_external.txt @@ -0,0 +1,4 @@ +# this file is generated by inline-snapshot and requires no manual edits (https://15r10nk.github.io/inline-snapshot/latest/external/external/#cleaning-up-old-externals) +tests/lib/_parse/test_beta_messages.py +tests/lib/_parse/test_messages.py +tests/lib/tools/test_runners.py diff --git a/src/anthropic/lib/_parse/_transform.py b/src/anthropic/lib/_parse/_transform.py index ce0c83ac9..6be411292 100644 --- a/src/anthropic/lib/_parse/_transform.py +++ b/src/anthropic/lib/_parse/_transform.py @@ -51,6 +51,71 @@ def get_transformed_string( return schema +def transform_schema( + json_schema: type[pydantic.BaseModel] | dict[str, Any], +) -> dict[str, Any]: + """ + Transforms a JSON schema to ensure it conforms to the API's expectations. + + Args: + json_schema (Dict[str, Any]): The original JSON schema. + + Returns: + The transformed JSON schema. + + Examples: + >>> transform_schema( + ... { + ... "type": "integer", + ... "minimum": 1, + ... "maximum": 10, + ... "description": "A number", + ... } + ... ) + {'type': 'integer', 'description': 'A number\n\n{minimum: 1, maximum: 10}'} + """ + if inspect.isclass(json_schema) and issubclass(json_schema, pydantic.BaseModel): # pyright: ignore[reportUnnecessaryIsInstance] + json_schema = json_schema.model_json_schema() + + strict_schema: dict[str, Any] = {} + json_schema = {**json_schema} + + # $defs must be processed before the $ref early-return below, so that a + # root-level `{"$ref": "#/$defs/X", "$defs": {...}}` (valid JSON Schema, + # and what pydantic RootModel emits) keeps its definitions. + defs = json_schema.pop("$defs", None) + if defs is not None: + strict_defs: dict[str, Any] = {} + strict_schema["$defs"] = strict_defs + + for name, schema in defs.items(): + strict_defs[name] = transform_schema(schema) + + ref = json_schema.pop("$ref", None) + if ref is not None: + strict_schema["$ref"] = ref + return strict_schema + + def _normalize_type_array(type_list: list[str]) -> list[dict[str, Any]]: + """ + Convert a type array (e.g., ["string", "null"]) to an anyOf array. + + Each type in the array is converted to a simple schema with that type. + """ + normalized: list[dict[str, Any]] = [] + seen: set[str] = set() + for t in type_list: + if not isinstance(t, str): + raise ValueError(f"Type array members must be strings, got {type(t).__name__}") + if t not in SupportedTypes.__args__: + raise ValueError(f"Unsupported type in type array: {t}") + if t in seen: + raise ValueError(f"Duplicate type in type array: {t}") + seen.add(t) + normalized.append({"type": t}) + return normalized + + def transform_schema( json_schema: type[pydantic.BaseModel] | dict[str, Any], ) -> dict[str, Any]: @@ -101,6 +166,13 @@ def transform_schema( one_of = json_schema.pop("oneOf", None) all_of = json_schema.pop("allOf", None) + # Handle type arrays (e.g., ["string", "null"]) by converting to anyOf + if is_list(type_): + if not type_: + raise ValueError("Type array cannot be empty") + any_of = _normalize_type_array(type_) + type_ = None + if is_list(any_of): strict_schema["anyOf"] = [transform_schema(cast("dict[str, Any]", variant)) for variant in any_of] elif is_list(one_of):