From 884edbfb09866268f6c4c57d09192246b02649b0 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:05:16 +0100 Subject: [PATCH 1/3] Fail clearly on unsupported dynamic object schemas --- src/anthropic/lib/_parse/_transform.py | 34 +++++++++++- .../_parse/test_dynamic_object_transform.py | 24 +++++++++ tests/lib/_parse/test_transform.py | 52 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 tests/lib/_parse/test_dynamic_object_transform.py diff --git a/src/anthropic/lib/_parse/_transform.py b/src/anthropic/lib/_parse/_transform.py index ce0c83ac9..d80b1fc00 100644 --- a/src/anthropic/lib/_parse/_transform.py +++ b/src/anthropic/lib/_parse/_transform.py @@ -51,6 +51,27 @@ def get_transformed_string( return schema +def _dynamic_object_keywords_without_properties(json_schema: dict[str, Any]) -> list[str]: + """Return unsupported keywords that are the only way this object can admit keys.""" + keywords: list[str] = [] + + pattern_properties = json_schema.get("patternProperties") + if isinstance(pattern_properties, dict) and pattern_properties: + keywords.append("patternProperties") + + additional_properties = json_schema.get("additionalProperties") + if isinstance(additional_properties, dict): + keywords.append("additionalProperties") + + if "propertyNames" in json_schema and additional_properties is not False: + keywords.append("propertyNames") + + if "unevaluatedProperties" in json_schema and json_schema["unevaluatedProperties"] is not False: + keywords.append("unevaluatedProperties") + + return keywords + + def transform_schema( json_schema: type[pydantic.BaseModel] | dict[str, Any], ) -> dict[str, Any]: @@ -126,8 +147,19 @@ def transform_schema( strict_schema["title"] = title if type_ == "object": + properties = json_schema.pop("properties", {}) + if not properties: + unsupported_dynamic_keywords = _dynamic_object_keywords_without_properties(json_schema) + if unsupported_dynamic_keywords: + keywords = ", ".join(unsupported_dynamic_keywords) + raise ValueError( + "Structured output schemas cannot safely transform an object that relies on " + f"{keywords} without explicit properties. Transforming this schema would make " + "the object accept no keys." + ) + strict_schema["properties"] = { - key: transform_schema(prop_schema) for key, prop_schema in json_schema.pop("properties", {}).items() + key: transform_schema(prop_schema) for key, prop_schema in properties.items() } json_schema.pop("additionalProperties", None) strict_schema["additionalProperties"] = False diff --git a/tests/lib/_parse/test_dynamic_object_transform.py b/tests/lib/_parse/test_dynamic_object_transform.py new file mode 100644 index 000000000..fda2f5d17 --- /dev/null +++ b/tests/lib/_parse/test_dynamic_object_transform.py @@ -0,0 +1,24 @@ +import pytest +from pydantic import BaseModel + +from anthropic.lib._parse._transform import transform_schema + + +def test_additional_properties_map_raises_instead_of_becoming_empty_object(): + schema = { + "type": "object", + "additionalProperties": {"type": "string"}, + } + + with pytest.raises(ValueError, match="additionalProperties"): + transform_schema(schema) + + +def test_pydantic_dict_field_raises_instead_of_becoming_empty_object(): + class Model(BaseModel): + values: dict[str, str] + + schema = Model.model_json_schema()["properties"]["values"] + + with pytest.raises(ValueError, match="additionalProperties"): + transform_schema(schema) diff --git a/tests/lib/_parse/test_transform.py b/tests/lib/_parse/test_transform.py index 7a2799dce..a5af5fb05 100644 --- a/tests/lib/_parse/test_transform.py +++ b/tests/lib/_parse/test_transform.py @@ -229,3 +229,55 @@ def test_original_schema_not_mutated(): transform_schema(original_schema) assert original_schema == original_schema_backup + + +@pytest.mark.parametrize( + ("keyword", "constraint"), + [ + ("patternProperties", {"^S_": {"type": "string"}}), + ("propertyNames", {"pattern": "^S_"}), + ("unevaluatedProperties", {"type": "string"}), + ], +) +def test_dynamic_object_schema_without_properties_raises(keyword: str, constraint: object): + schema = {"type": "object", keyword: constraint} + + with pytest.raises(ValueError, match=keyword): + transform_schema(schema) + + +def test_pattern_properties_with_explicit_properties_keeps_existing_demotion_behavior(): + schema = { + "type": "object", + "properties": {"fixed": {"type": "string"}}, + "required": ["fixed"], + "patternProperties": {"^S_": {"type": "string"}}, + } + + result = transform_schema(schema) + + assert result == { + "type": "object", + "properties": {"fixed": {"type": "string"}}, + "additionalProperties": False, + "required": ["fixed"], + "description": "{patternProperties: {'^S_': {'type': 'string'}}}", + } + + +def test_closed_empty_object_is_not_rejected(): + schema = { + "type": "object", + "additionalProperties": False, + "propertyNames": {"pattern": "^S_"}, + "unevaluatedProperties": False, + } + + result = transform_schema(schema) + + assert result == { + "type": "object", + "properties": {}, + "additionalProperties": False, + "description": "{propertyNames: {'pattern': '^S_'}, unevaluatedProperties: False}", + } From 22fca9d9bc7ac91ada4251a469a114c064fcfc7c Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:20:33 +0100 Subject: [PATCH 2/3] fix(parse): reject open dynamic object schemas --- src/anthropic/lib/_parse/_transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anthropic/lib/_parse/_transform.py b/src/anthropic/lib/_parse/_transform.py index d80b1fc00..fe82a4c9e 100644 --- a/src/anthropic/lib/_parse/_transform.py +++ b/src/anthropic/lib/_parse/_transform.py @@ -60,7 +60,7 @@ def _dynamic_object_keywords_without_properties(json_schema: dict[str, Any]) -> keywords.append("patternProperties") additional_properties = json_schema.get("additionalProperties") - if isinstance(additional_properties, dict): + if additional_properties is True or isinstance(additional_properties, dict): keywords.append("additionalProperties") if "propertyNames" in json_schema and additional_properties is not False: From def8b3d83b0987e25158df308bbbb6a17a2209be Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:20:40 +0100 Subject: [PATCH 3/3] test(parse): cover arbitrary dict schemas --- .../_parse/test_dynamic_object_transform.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/lib/_parse/test_dynamic_object_transform.py b/tests/lib/_parse/test_dynamic_object_transform.py index fda2f5d17..edcfb39ab 100644 --- a/tests/lib/_parse/test_dynamic_object_transform.py +++ b/tests/lib/_parse/test_dynamic_object_transform.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from pydantic import BaseModel @@ -14,7 +16,17 @@ def test_additional_properties_map_raises_instead_of_becoming_empty_object(): transform_schema(schema) -def test_pydantic_dict_field_raises_instead_of_becoming_empty_object(): +def test_additional_properties_true_raises_instead_of_becoming_empty_object(): + schema = { + "type": "object", + "additionalProperties": True, + } + + with pytest.raises(ValueError, match="additionalProperties"): + transform_schema(schema) + + +def test_pydantic_typed_dict_field_raises_instead_of_becoming_empty_object(): class Model(BaseModel): values: dict[str, str] @@ -22,3 +34,13 @@ class Model(BaseModel): with pytest.raises(ValueError, match="additionalProperties"): transform_schema(schema) + + +def test_pydantic_arbitrary_dict_field_raises_instead_of_becoming_empty_object(): + class Model(BaseModel): + values: dict[str, Any] + + schema = Model.model_json_schema()["properties"]["values"] + + with pytest.raises(ValueError, match="additionalProperties"): + transform_schema(schema)