From e4d9c24922e39d7ad1c878b3f0ab50f554e9d9e4 Mon Sep 17 00:00:00 2001 From: William Easton Date: Sat, 11 Apr 2026 13:25:48 -0500 Subject: [PATCH 1/7] Handle allOf and oneOf in json_schema_to_type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously these compositions silently resolved to Any, disabling all validation for schemas that use them. allOf is now merged into a single object schema; oneOf creates a Union type. 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fastmcp/utilities/json_schema_type.py | 75 ++++++- .../json_schema_type/test_json_schema_type.py | 206 ++++++++++++++++++ 2 files changed, 280 insertions(+), 1 deletion(-) diff --git a/fastmcp_slim/fastmcp/utilities/json_schema_type.py b/fastmcp_slim/fastmcp/utilities/json_schema_type.py index 140fc6041..a079fd9a3 100644 --- a/fastmcp_slim/fastmcp/utilities/json_schema_type.py +++ b/fastmcp_slim/fastmcp/utilities/json_schema_type.py @@ -453,7 +453,7 @@ def _schema_to_type( if not schema: return object - if "type" not in schema and "properties" in schema: + if "type" not in schema and "properties" in schema and "allOf" not in schema: return _create_dataclass(schema, schema.get("title", ""), schemas) # Handle references first @@ -493,6 +493,79 @@ def _schema_to_type( else: return Union[tuple(types)] # type: ignore # noqa: UP007 + # Handle allOf (schema intersection / composition). + # Flatten all sub-schemas into a single merged object schema. + if "allOf" in schema: + merged: dict[str, Any] = {} + merged_properties: dict[str, Any] = {} + merged_required: list[str] = [] + + def _collect_allof(sub: Any) -> None: + """Recursively collect properties from a sub-schema.""" + if isinstance(sub, bool): + return + if "$ref" in sub: + sub = dict(_resolve_ref(sub["$ref"], schemas)) + # Recurse into nested allOf + if "allOf" in sub: + for nested in sub["allOf"]: + _collect_allof(nested) + merged_properties.update(sub.get("properties", {})) + merged_required.extend(sub.get("required", [])) + for key in ("title", "description", "additionalProperties"): + if key in sub and key not in merged: + merged[key] = sub[key] + + # Include sibling properties/required from the schema itself, + # not just from allOf children. This handles schemas like + # {"properties": {"local": ...}, "allOf": [{"properties": {"inherited": ...}}]} + merged_properties.update(schema.get("properties", {})) + merged_required.extend(schema.get("required", [])) + + for sub in schema["allOf"]: + _collect_allof(sub) + if merged_properties: + merged["type"] = "object" + merged["properties"] = merged_properties + if merged_required: + merged["required"] = list(dict.fromkeys(merged_required)) + return _schema_to_type(merged, schemas) + # allOf with no mergeable properties — fall through to Any + + # Handle oneOf (exactly-one-of union). + # Treat like anyOf for type construction — Pydantic's Union + # does "first match" which is a reasonable approximation. + if "oneOf" in schema: + types: list[type | Any] = [] + for subschema in schema["oneOf"]: + # Same dict special-case as the anyOf handler: detect + # map-like objects so they become dict[str, X] instead + # of empty dataclasses that discard key/value data. + if ( + isinstance(subschema, dict) + and subschema.get("type") == "object" + and not subschema.get("properties") + and subschema.get("additionalProperties") + ): + additional_props = subschema["additionalProperties"] + if additional_props is True: + types.append(dict[str, Any]) + else: + value_type = _schema_to_type(additional_props, schemas) + types.append(dict[str, value_type]) # type: ignore + else: + types.append(_schema_to_type(subschema, schemas)) + has_null = type(None) in types + types = [t for t in types if t is not type(None)] + if len(types) == 0: + return type(None) + elif len(types) == 1: + return types[0] | None if has_null else types[0] # type: ignore + else: + if has_null: + return Union[(*types, type(None))] # type: ignore + return Union[tuple(types)] # type: ignore # noqa: UP007 + schema_type = schema.get("type") if not schema_type: return Any diff --git a/tests/utilities/json_schema_type/test_json_schema_type.py b/tests/utilities/json_schema_type/test_json_schema_type.py index c125ddc98..e46193305 100644 --- a/tests/utilities/json_schema_type/test_json_schema_type.py +++ b/tests/utilities/json_schema_type/test_json_schema_type.py @@ -404,3 +404,209 @@ class TestUnsupportedPatternFallback: warnings.simplefilter("error", UserWarning) T = json_schema_to_type(schema) # must not warn TypeAdapter(T).validate_python("hello") + +class TestAllOfOneOf: + """allOf and oneOf composition, previously returning Any.""" + + def test_allof_merges_properties(self): + """allOf sub-schemas should be merged into a single object type.""" + schema = { + "allOf": [ + { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + { + "type": "object", + "properties": {"age": {"type": "integer"}}, + }, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + result = ta.validate_python({"name": "Alice", "age": 30}) + assert result.name == "Alice" # ty:ignore[unresolved-attribute] + assert result.age == 30 # ty:ignore[unresolved-attribute] + + def test_allof_preserves_required(self): + """Required fields from all allOf sub-schemas should be enforced.""" + schema = { + "allOf": [ + { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + { + "type": "object", + "properties": {"age": {"type": "integer"}}, + }, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + with pytest.raises(ValidationError): + ta.validate_python({"age": 30}) # missing required 'name' + + def test_allof_with_ref(self): + """allOf with $ref should resolve and merge the referenced schema.""" + schema = { + "allOf": [ + {"$ref": "#/$defs/Base"}, + { + "type": "object", + "properties": {"extra": {"type": "string"}}, + }, + ], + "$defs": { + "Base": { + "type": "object", + "properties": {"id": {"type": "integer"}}, + "required": ["id"], + } + }, + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + result = ta.validate_python({"id": 1, "extra": "hello"}) + assert result.id == 1 # ty:ignore[unresolved-attribute] + assert result.extra == "hello" # ty:ignore[unresolved-attribute] + + def test_oneof_creates_union(self): + """oneOf should create a Union type that accepts any sub-schema.""" + schema = { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": {"const": "dog"}, + "breed": {"type": "string"}, + }, + "required": ["kind", "breed"], + }, + { + "type": "object", + "properties": { + "kind": {"const": "cat"}, + "indoor": {"type": "boolean"}, + }, + "required": ["kind", "indoor"], + }, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + dog = ta.validate_python({"kind": "dog", "breed": "lab"}) + assert dog.kind == "dog" # ty:ignore[unresolved-attribute] + + cat = ta.validate_python({"kind": "cat", "indoor": True}) + assert cat.indoor is True # ty:ignore[unresolved-attribute] + + def test_oneof_with_scalars(self): + """oneOf with scalar types should create a Union.""" + schema = { + "oneOf": [ + {"type": "string"}, + {"type": "integer"}, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + assert ta.validate_python("hello") == "hello" + assert ta.validate_python(42) == 42 + + def test_nested_allof(self): + """allOf inside allOf should be flattened recursively.""" + schema = { + "allOf": [ + { + "allOf": [ + {"type": "object", "properties": {"a": {"type": "string"}}}, + {"type": "object", "properties": {"b": {"type": "integer"}}}, + ] + }, + {"type": "object", "properties": {"c": {"type": "boolean"}}}, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + result = ta.validate_python({"a": "x", "b": 1, "c": True}) + assert result.a == "x" # ty:ignore[unresolved-attribute] + assert result.b == 1 # ty:ignore[unresolved-attribute] + assert result.c is True # ty:ignore[unresolved-attribute] + + def test_allof_ref_to_allof(self): + """allOf with $ref pointing to another allOf should resolve fully.""" + schema = { + "allOf": [ + {"$ref": "#/$defs/Combined"}, + {"type": "object", "properties": {"extra": {"type": "string"}}}, + ], + "$defs": { + "Combined": { + "allOf": [ + { + "type": "object", + "properties": {"x": {"type": "integer"}}, + "required": ["x"], + }, + { + "type": "object", + "properties": {"y": {"type": "integer"}}, + "required": ["y"], + }, + ] + } + }, + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + result = ta.validate_python({"x": 1, "y": 2, "extra": "hi"}) + assert result.x == 1 # ty:ignore[unresolved-attribute] + assert result.y == 2 # ty:ignore[unresolved-attribute] + assert result.extra == "hi" # ty:ignore[unresolved-attribute] + + def test_allof_with_sibling_properties(self): + """Sibling properties on the same schema as allOf should be included.""" + schema = { + "properties": {"local": {"type": "string"}}, + "required": ["local"], + "allOf": [ + { + "type": "object", + "properties": {"inherited": {"type": "integer"}}, + } + ], + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + result = ta.validate_python({"local": "hi", "inherited": 42}) + assert result.local == "hi" # ty:ignore[unresolved-attribute] + assert result.inherited == 42 # ty:ignore[unresolved-attribute] + + with pytest.raises(ValidationError): + ta.validate_python({"inherited": 42}) # missing required 'local' + + def test_oneof_with_dict_branch(self): + """oneOf with a map-like object branch should produce dict[str, X].""" + schema = { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "additionalProperties": {"type": "integer"}, + }, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + assert ta.validate_python("hello") == "hello" + assert ta.validate_python({"x": 1, "y": 2}) == {"x": 1, "y": 2} From a3927d556dcc50ab8aae2bd6b8aeb3f7c96d1807 Mon Sep 17 00:00:00 2001 From: strawgate Date: Sat, 25 Apr 2026 18:17:44 -0500 Subject: [PATCH 2/7] fix: handle boolean schemas and false branches in allOf/oneOf - Fix dict() crash when $ref resolves to boolean schema (draft-06+) - Treat allOf containing 'false' as unsatisfiable (reject all values) - Add tests for boolean ref, false sub-schema, and false-only allOf - additionalProperties preservation from parent noted as pre-existing limitation --- .../fastmcp/utilities/json_schema_type.py | 26 ++++++++++++- .../json_schema_type/test_json_schema_type.py | 38 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/fastmcp_slim/fastmcp/utilities/json_schema_type.py b/fastmcp_slim/fastmcp/utilities/json_schema_type.py index a079fd9a3..3bd70c04a 100644 --- a/fastmcp_slim/fastmcp/utilities/json_schema_type.py +++ b/fastmcp_slim/fastmcp/utilities/json_schema_type.py @@ -499,13 +499,25 @@ def _schema_to_type( merged: dict[str, Any] = {} merged_properties: dict[str, Any] = {} merged_required: list[str] = [] + has_false = False def _collect_allof(sub: Any) -> None: """Recursively collect properties from a sub-schema.""" + nonlocal has_false + if sub is False: + has_false = True + return + if sub is True: + return if isinstance(sub, bool): return if "$ref" in sub: - sub = dict(_resolve_ref(sub["$ref"], schemas)) + resolved = _resolve_ref(sub["$ref"], schemas) + if isinstance(resolved, bool): + if resolved is False: + has_false = True + return + sub = dict(resolved) # Recurse into nested allOf if "allOf" in sub: for nested in sub["allOf"]: @@ -524,11 +536,23 @@ def _schema_to_type( for sub in schema["allOf"]: _collect_allof(sub) + + if has_false: + return _UnsatisfiableType # type: ignore[return-value] + if merged_properties: merged["type"] = "object" merged["properties"] = merged_properties if merged_required: merged["required"] = list(dict.fromkeys(merged_required)) + # Preserve additionalProperties from the parent schema, not just + # from allOf children, so schemas like + # {"additionalProperties": true, "allOf": [...]} allow extra keys. + if ( + "additionalProperties" not in merged + and "additionalProperties" in schema + ): + merged["additionalProperties"] = schema["additionalProperties"] return _schema_to_type(merged, schemas) # allOf with no mergeable properties — fall through to Any diff --git a/tests/utilities/json_schema_type/test_json_schema_type.py b/tests/utilities/json_schema_type/test_json_schema_type.py index e46193305..842cf53d1 100644 --- a/tests/utilities/json_schema_type/test_json_schema_type.py +++ b/tests/utilities/json_schema_type/test_json_schema_type.py @@ -610,3 +610,41 @@ class TestAllOfOneOf: assert ta.validate_python("hello") == "hello" assert ta.validate_python({"x": 1, "y": 2}) == {"x": 1, "y": 2} + + def test_allof_false_sub_schema_is_unsatisfiable(self): + """allOf containing `false` should produce an unsatisfiable type.""" + schema = { + "allOf": [ + {"type": "object", "properties": {"name": {"type": "string"}}}, + False, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + with pytest.raises(ValidationError): + ta.validate_python({"name": "Alice"}) + + def test_allof_only_false_is_unsatisfiable(self): + """allOf with only `false` should produce an unsatisfiable type.""" + schema = {"allOf": [False]} + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + with pytest.raises(ValidationError): + ta.validate_python({"any": "value"}) + + def test_allof_ref_to_boolean_schema(self): + """allOf with a $ref resolving to `false` should be unsatisfiable.""" + schema = { + "allOf": [ + {"$ref": "#/$defs/Never"}, + {"type": "object", "properties": {"name": {"type": "string"}}}, + ], + "$defs": {"Never": False}, + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + + with pytest.raises(ValidationError): + ta.validate_python({"name": "Alice"}) From 08236c4345ce58a3c9b50f0e080c45a6e719cb38 Mon Sep 17 00:00:00 2001 From: strawgate Date: Tue, 12 May 2026 22:48:36 -0500 Subject: [PATCH 3/7] fix: address review feedback on allOf/oneOf handling - Guard property+oneOf schemas from premature dataclass return - Recognize empty additionalProperties ({}) as allow-any in oneOf - Boolean false allOf branches and sibling properties already handled Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fastmcp_slim/fastmcp/utilities/json_schema_type.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastmcp_slim/fastmcp/utilities/json_schema_type.py b/fastmcp_slim/fastmcp/utilities/json_schema_type.py index 3bd70c04a..b3163872b 100644 --- a/fastmcp_slim/fastmcp/utilities/json_schema_type.py +++ b/fastmcp_slim/fastmcp/utilities/json_schema_type.py @@ -453,7 +453,7 @@ def _schema_to_type( if not schema: return object - if "type" not in schema and "properties" in schema and "allOf" not in schema: + if "type" not in schema and "properties" in schema and "allOf" not in schema and "oneOf" not in schema: return _create_dataclass(schema, schema.get("title", ""), schemas) # Handle references first @@ -569,10 +569,10 @@ def _schema_to_type( isinstance(subschema, dict) and subschema.get("type") == "object" and not subschema.get("properties") - and subschema.get("additionalProperties") + and "additionalProperties" in subschema ): additional_props = subschema["additionalProperties"] - if additional_props is True: + if additional_props is True or additional_props == {}: types.append(dict[str, Any]) else: value_type = _schema_to_type(additional_props, schemas) From 16b48452b131f4fd9356e56bed937dd5e1f3dc6b Mon Sep 17 00:00:00 2001 From: strawgate Date: Tue, 12 May 2026 23:00:56 -0500 Subject: [PATCH 4/7] Fix ruff ERA001 and format issues Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fastmcp_slim/fastmcp/utilities/json_schema_type.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/fastmcp_slim/fastmcp/utilities/json_schema_type.py b/fastmcp_slim/fastmcp/utilities/json_schema_type.py index b3163872b..d2f7d3172 100644 --- a/fastmcp_slim/fastmcp/utilities/json_schema_type.py +++ b/fastmcp_slim/fastmcp/utilities/json_schema_type.py @@ -453,7 +453,12 @@ def _schema_to_type( if not schema: return object - if "type" not in schema and "properties" in schema and "allOf" not in schema and "oneOf" not in schema: + if ( + "type" not in schema + and "properties" in schema + and "allOf" not in schema + and "oneOf" not in schema + ): return _create_dataclass(schema, schema.get("title", ""), schemas) # Handle references first @@ -529,8 +534,8 @@ def _schema_to_type( merged[key] = sub[key] # Include sibling properties/required from the schema itself, - # not just from allOf children. This handles schemas like - # {"properties": {"local": ...}, "allOf": [{"properties": {"inherited": ...}}]} + # not just from allOf children — covers schemas where top-level + # properties coexist with an allOf list of inherited properties. merged_properties.update(schema.get("properties", {})) merged_required.extend(schema.get("required", [])) From 087d4b3b849750b882815ed168b7363a05fbdc28 Mon Sep 17 00:00:00 2001 From: strawgate Date: Tue, 12 May 2026 23:06:45 -0500 Subject: [PATCH 5/7] Fix ruff format in test file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/utilities/json_schema_type/test_json_schema_type.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/utilities/json_schema_type/test_json_schema_type.py b/tests/utilities/json_schema_type/test_json_schema_type.py index 842cf53d1..8a8213f6a 100644 --- a/tests/utilities/json_schema_type/test_json_schema_type.py +++ b/tests/utilities/json_schema_type/test_json_schema_type.py @@ -405,6 +405,7 @@ class TestUnsupportedPatternFallback: T = json_schema_to_type(schema) # must not warn TypeAdapter(T).validate_python("hello") + class TestAllOfOneOf: """allOf and oneOf composition, previously returning Any.""" From 025ed3594f7c60f171a193baff0f6762ac002e56 Mon Sep 17 00:00:00 2001 From: strawgate Date: Wed, 13 May 2026 00:59:33 -0500 Subject: [PATCH 6/7] Fix allOf/oneOf edge cases and add comprehensive tests Bugs fixed: - additionalProperties: {} now treated as true (per JSON Schema spec) - Sibling properties override allOf children (local > inherited) - additionalProperties: false wins when allOf children conflict - anyOf guard added to typeless-properties shortcircuit New test coverage: - Empty additionalProperties schema (with and without properties) - Sibling property precedence over allOf children - additionalProperties intersection in allOf - oneOf with null branch - properties + anyOf not shortcircuited - allOf with only required (no properties) - oneOf with empty additionalProperties dict - allOf with sibling additionalProperties: true Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../fastmcp/utilities/json_schema_type.py | 25 ++- .../json_schema_type/test_json_schema_type.py | 142 ++++++++++++++++++ 2 files changed, 160 insertions(+), 7 deletions(-) diff --git a/fastmcp_slim/fastmcp/utilities/json_schema_type.py b/fastmcp_slim/fastmcp/utilities/json_schema_type.py index d2f7d3172..2a0dbd386 100644 --- a/fastmcp_slim/fastmcp/utilities/json_schema_type.py +++ b/fastmcp_slim/fastmcp/utilities/json_schema_type.py @@ -403,6 +403,10 @@ def _object_schema_to_type( """ has_properties = bool(schema.get("properties")) additional_props = schema.get("additionalProperties") + # Per JSON Schema, an empty schema {} means "accept anything" — + # equivalent to additionalProperties: true. + if isinstance(additional_props, dict) and not additional_props: + additional_props = True class_name = name if name is not None else schema.get("title") if not has_properties and additional_props: @@ -458,6 +462,7 @@ def _schema_to_type( and "properties" in schema and "allOf" not in schema and "oneOf" not in schema + and "anyOf" not in schema ): return _create_dataclass(schema, schema.get("title", ""), schemas) @@ -529,19 +534,25 @@ def _schema_to_type( _collect_allof(nested) merged_properties.update(sub.get("properties", {})) merged_required.extend(sub.get("required", [])) - for key in ("title", "description", "additionalProperties"): + for key in ("title", "description"): if key in sub and key not in merged: merged[key] = sub[key] + # Intersect additionalProperties: false is most restrictive and wins. + if "additionalProperties" in sub: + existing = merged.get("additionalProperties") + if existing is None: + merged["additionalProperties"] = sub["additionalProperties"] + elif sub["additionalProperties"] is False: + merged["additionalProperties"] = False - # Include sibling properties/required from the schema itself, - # not just from allOf children — covers schemas where top-level - # properties coexist with an allOf list of inherited properties. - merged_properties.update(schema.get("properties", {})) - merged_required.extend(schema.get("required", [])) - + # Collect from allOf children first, then overlay sibling + # properties so local definitions take precedence over inherited. for sub in schema["allOf"]: _collect_allof(sub) + merged_properties.update(schema.get("properties", {})) + merged_required.extend(schema.get("required", [])) + if has_false: return _UnsatisfiableType # type: ignore[return-value] diff --git a/tests/utilities/json_schema_type/test_json_schema_type.py b/tests/utilities/json_schema_type/test_json_schema_type.py index 8a8213f6a..fd673e038 100644 --- a/tests/utilities/json_schema_type/test_json_schema_type.py +++ b/tests/utilities/json_schema_type/test_json_schema_type.py @@ -649,3 +649,145 @@ class TestAllOfOneOf: with pytest.raises(ValidationError): ta.validate_python({"name": "Alice"}) + + def test_additional_properties_empty_schema_is_allow_any(self): + """additionalProperties: {} is equivalent to additionalProperties: true per spec.""" + schema = { + "type": "object", + "additionalProperties": {}, + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + # Should accept any values (empty schema = true = allow everything) + assert ta.validate_python({"x": 1, "y": "two"}) == {"x": 1, "y": "two"} + + def test_additional_properties_empty_schema_with_properties(self): + """properties + additionalProperties: {} should produce Pydantic model (extra=allow).""" + from pydantic import BaseModel + + schema = { + "type": "object", + "properties": {"name": {"type": "string"}}, + "additionalProperties": {}, + } + T = json_schema_to_type(schema) + assert issubclass(T, BaseModel) + ta = TypeAdapter(T) + result = ta.validate_python({"name": "Alice", "extra_key": 42}) + assert result.name == "Alice" + + def test_sibling_properties_override_allof(self): + """Sibling properties should take precedence over allOf children.""" + schema = { + "properties": {"name": {"type": "integer"}}, + "allOf": [ + {"type": "object", "properties": {"name": {"type": "string"}}}, + ], + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + # Sibling says name is int, allOf child says str — sibling wins + result = ta.validate_python({"name": 42}) + assert result.name == 42 + + def test_allof_additional_properties_false_wins(self): + """When allOf children conflict on additionalProperties, false should win.""" + schema = { + "allOf": [ + { + "type": "object", + "properties": {"a": {"type": "string"}}, + "additionalProperties": True, + }, + { + "type": "object", + "properties": {"b": {"type": "integer"}}, + "additionalProperties": False, + }, + ], + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + result = ta.validate_python({"a": "hello", "b": 1}) + assert result.a == "hello" + assert result.b == 1 + + def test_oneof_with_null_branch(self): + """oneOf with null branch should produce Optional type.""" + schema = { + "oneOf": [ + {"type": "string"}, + {"type": "null"}, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + assert ta.validate_python("hello") == "hello" + assert ta.validate_python(None) is None + + def test_properties_with_anyof_not_shortcircuited(self): + """Schema with properties + anyOf should NOT shortcircuit to dataclass.""" + schema = { + "properties": {"base": {"type": "string"}}, + "anyOf": [ + {"properties": {"variant_a": {"type": "integer"}}}, + {"properties": {"variant_b": {"type": "boolean"}}}, + ], + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + result = ta.validate_python({"base": "val"}) + assert result is not None + + def test_allof_with_only_required_no_properties(self): + """allOf child with only required (no properties) merges correctly.""" + schema = { + "allOf": [ + { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + }, + {"required": ["name"]}, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + result = ta.validate_python({"name": "Alice"}) + assert result.name == "Alice" + with pytest.raises(ValidationError): + ta.validate_python({"age": 30}) + + def test_oneof_with_empty_additional_properties_dict(self): + """oneOf branch with additionalProperties: {} should produce dict[str, Any].""" + schema = { + "oneOf": [ + {"type": "string"}, + {"type": "object", "additionalProperties": {}}, + ] + } + T = json_schema_to_type(schema) + ta = TypeAdapter(T) + assert ta.validate_python("hello") == "hello" + assert ta.validate_python({"x": 1}) == {"x": 1} + + def test_allof_with_sibling_additional_properties_true(self): + """properties + additionalProperties: true + allOf should preserve extra keys.""" + from pydantic import BaseModel + + schema = { + "additionalProperties": True, + "allOf": [ + { + "type": "object", + "properties": {"name": {"type": "string"}}, + }, + ], + } + T = json_schema_to_type(schema) + assert issubclass(T, BaseModel) + ta = TypeAdapter(T) + result = ta.validate_python({"name": "Alice", "extra": "kept"}) + assert result.name == "Alice" From f9543964defd4497f8dc54c35dd596bee695dee9 Mon Sep 17 00:00:00 2001 From: strawgate Date: Wed, 13 May 2026 01:08:01 -0500 Subject: [PATCH 7/7] Add ty:ignore comments for dynamic attribute access in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../json_schema_type/test_json_schema_type.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/utilities/json_schema_type/test_json_schema_type.py b/tests/utilities/json_schema_type/test_json_schema_type.py index fd673e038..7fc2f6640 100644 --- a/tests/utilities/json_schema_type/test_json_schema_type.py +++ b/tests/utilities/json_schema_type/test_json_schema_type.py @@ -674,7 +674,7 @@ class TestAllOfOneOf: assert issubclass(T, BaseModel) ta = TypeAdapter(T) result = ta.validate_python({"name": "Alice", "extra_key": 42}) - assert result.name == "Alice" + assert result.name == "Alice" # ty: ignore[unresolved-attribute] def test_sibling_properties_override_allof(self): """Sibling properties should take precedence over allOf children.""" @@ -688,7 +688,7 @@ class TestAllOfOneOf: ta = TypeAdapter(T) # Sibling says name is int, allOf child says str — sibling wins result = ta.validate_python({"name": 42}) - assert result.name == 42 + assert result.name == 42 # ty: ignore[unresolved-attribute] def test_allof_additional_properties_false_wins(self): """When allOf children conflict on additionalProperties, false should win.""" @@ -709,8 +709,8 @@ class TestAllOfOneOf: T = json_schema_to_type(schema) ta = TypeAdapter(T) result = ta.validate_python({"a": "hello", "b": 1}) - assert result.a == "hello" - assert result.b == 1 + assert result.a == "hello" # ty: ignore[unresolved-attribute] + assert result.b == 1 # ty: ignore[unresolved-attribute] def test_oneof_with_null_branch(self): """oneOf with null branch should produce Optional type.""" @@ -756,7 +756,7 @@ class TestAllOfOneOf: T = json_schema_to_type(schema) ta = TypeAdapter(T) result = ta.validate_python({"name": "Alice"}) - assert result.name == "Alice" + assert result.name == "Alice" # ty: ignore[unresolved-attribute] with pytest.raises(ValidationError): ta.validate_python({"age": 30}) @@ -790,4 +790,4 @@ class TestAllOfOneOf: assert issubclass(T, BaseModel) ta = TypeAdapter(T) result = ta.validate_python({"name": "Alice", "extra": "kept"}) - assert result.name == "Alice" + assert result.name == "Alice" # ty: ignore[unresolved-attribute]