diff --git a/src/anthropic/lib/_parse/_transform.py b/src/anthropic/lib/_parse/_transform.py index ce0c83ac..982e8375 100644 --- a/src/anthropic/lib/_parse/_transform.py +++ b/src/anthropic/lib/_parse/_transform.py @@ -107,11 +107,11 @@ def transform_schema( strict_schema["anyOf"] = [transform_schema(cast("dict[str, Any]", variant)) for variant in one_of] elif is_list(all_of): strict_schema["allOf"] = [transform_schema(cast("dict[str, Any]", variant)) for variant in all_of] - else: - if type_ is None: - raise ValueError("Schema must have a 'type', 'anyOf', 'oneOf', or 'allOf' field.") - + elif type_ is not None: strict_schema["type"] = type_ + # else: a typeless schema with no combinators — e.g. an empty `{}`, which pydantic + # emits for an `Any`-typed field or the items of an untyped `list`/`List[Any]`. This + # is a valid, unconstrained ("any") schema, so leave it untyped rather than raising. enum = json_schema.pop("enum", None) if is_list(enum): diff --git a/tests/lib/_parse/test_transform.py b/tests/lib/_parse/test_transform.py index 7a2799dc..e6ad00d7 100644 --- a/tests/lib/_parse/test_transform.py +++ b/tests/lib/_parse/test_transform.py @@ -1,6 +1,8 @@ from copy import deepcopy +from typing import Any, List import pytest +import pydantic from inline_snapshot import snapshot from anthropic.lib._parse._transform import transform_schema @@ -145,6 +147,57 @@ def test_array_schema(): ) +def test_empty_schema(): + # pydantic emits a bare `{}` for an `Any`-typed field; an empty schema is a + # valid, unconstrained ("any") schema and must not raise. + assert transform_schema({}) == snapshot({}) + + +def test_array_with_untyped_items(): + # `list` / `List[Any]` produces `{"type": "array", "items": {}}`. + schema: dict[str, Any] = {"type": "array", "items": {}} + result = transform_schema(schema) + assert result == snapshot({"type": "array", "items": {}}) + + +def test_object_with_any_property(): + schema: dict[str, Any] = {"type": "object", "properties": {"x": {}}, "required": ["x"], "title": "M"} + result = transform_schema(schema) + assert result == snapshot( + { + "type": "object", + "title": "M", + "properties": {"x": {}}, + "additionalProperties": False, + "required": ["x"], + } + ) + + +def test_model_with_untyped_list_and_any_field(): + # Regression: previously raised ValueError("Schema must have a 'type', ...") because + # the untyped `list` items / `Any` field transform to an empty `{}` sub-schema. + class Result(pydantic.BaseModel): + summary: str + tags: List[Any] + extra: Any + + result = transform_schema(Result) + assert result == snapshot( + { + "type": "object", + "title": "Result", + "properties": { + "summary": {"type": "string", "title": "Summary"}, + "tags": {"type": "array", "title": "Tags", "items": {}}, + "extra": {"title": "Extra"}, + }, + "additionalProperties": False, + "required": ["summary", "tags", "extra"], + } + ) + + def test_string_schema_with_format_and_default(): schema = { "type": "string",