From 2d3fdf5e5d568c9546a1239b361a8fed1000ca47 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 5 Mar 2026 17:10:19 +0000 Subject: [PATCH 1/3] Support parameter casting in composite schemas --- openapi_core/casting/schemas/__init__.py | 5 +++-- openapi_core/casting/schemas/casters.py | 26 +++++++++++++++++++++++ tests/unit/casting/test_schema_casters.py | 21 ++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/openapi_core/casting/schemas/__init__.py b/openapi_core/casting/schemas/__init__.py index 39c14a4e..1becd642 100644 --- a/openapi_core/casting/schemas/__init__.py +++ b/openapi_core/casting/schemas/__init__.py @@ -1,5 +1,6 @@ from collections import OrderedDict +from openapi_core.casting.schemas.casters import AnyCaster from openapi_core.casting.schemas.casters import ArrayCaster from openapi_core.casting.schemas.casters import BooleanCaster from openapi_core.casting.schemas.casters import IntegerCaster @@ -43,11 +44,11 @@ oas30_types_caster = TypesCaster( oas30_casters_dict, - PrimitiveCaster, + AnyCaster, ) oas31_types_caster = TypesCaster( oas31_casters_dict, - PrimitiveCaster, + AnyCaster, multi=PrimitiveCaster, ) oas32_types_caster = oas31_types_caster diff --git a/openapi_core/casting/schemas/casters.py b/openapi_core/casting/schemas/casters.py index 27e78e54..0e835d41 100644 --- a/openapi_core/casting/schemas/casters.py +++ b/openapi_core/casting/schemas/casters.py @@ -40,6 +40,32 @@ def cast(self, value: Any) -> Any: return value +class AnyCaster(PrimitiveCaster): + def cast(self, value: Any) -> Any: + if "allOf" in self.schema: + for subschema in self.schema / "allOf": + try: + value = self.schema_caster.evolve(subschema).cast(value) + except (ValueError, TypeError, CastError): + pass + + if "oneOf" in self.schema: + for subschema in self.schema / "oneOf": + try: + return self.schema_caster.evolve(subschema).cast(value) + except (ValueError, TypeError, CastError): + pass + + if "anyOf" in self.schema: + for subschema in self.schema / "anyOf": + try: + return self.schema_caster.evolve(subschema).cast(value) + except (ValueError, TypeError, CastError): + pass + + return value + + PrimitiveType = TypeVar("PrimitiveType") diff --git a/tests/unit/casting/test_schema_casters.py b/tests/unit/casting/test_schema_casters.py index 4e765cc8..da5a7333 100644 --- a/tests/unit/casting/test_schema_casters.py +++ b/tests/unit/casting/test_schema_casters.py @@ -66,3 +66,24 @@ def test_array_invalid_value(self, value, caster_factory): CastError, match=f"Failed to cast value to array type: {value}" ): caster_factory(schema).cast(value) + + @pytest.mark.parametrize( + "composite_type,schema_type,value,expected", + [ + ("allOf", "integer", "2", 2), + ("anyOf", "number", "3.14", 3.14), + ("oneOf", "boolean", "false", False), + ("oneOf", "boolean", "true", True), + ], + ) + def test_composite_primitive( + self, caster_factory, composite_type, schema_type, value, expected + ): + spec = { + composite_type: [{"type": schema_type}], + } + schema = SchemaPath.from_dict(spec) + + result = caster_factory(schema).cast(value) + + assert result == expected From 812cc8a756a95c8f0c42ebf8a068b080f50b54cc Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 5 Mar 2026 17:40:58 +0000 Subject: [PATCH 2/3] test: document known casting edge cases for composite schemas --- tests/unit/casting/test_schema_casters.py | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/unit/casting/test_schema_casters.py b/tests/unit/casting/test_schema_casters.py index da5a7333..bad8098e 100644 --- a/tests/unit/casting/test_schema_casters.py +++ b/tests/unit/casting/test_schema_casters.py @@ -87,3 +87,48 @@ def test_composite_primitive( result = caster_factory(schema).cast(value) assert result == expected + + @pytest.mark.parametrize( + "schemas,value,expected", + [ + # If string is evaluated first, it succeeds and returns string + ([{"type": "string"}, {"type": "integer"}], "123", "123"), + # If integer is evaluated first, it succeeds and returns int + ([{"type": "integer"}, {"type": "string"}], "123", 123), + ], + ) + def test_oneof_greedy_casting_edge_case( + self, caster_factory, schemas, value, expected + ): + """ + Documents the edge case that AnyCaster's oneOf/anyOf logic is greedy. + It returns the first successfully casted value based on the order in the list. + """ + spec = { + "oneOf": schemas, + } + schema = SchemaPath.from_dict(spec) + + result = caster_factory(schema).cast(value) + + assert result == expected + # Ensure exact type matches to prevent 123 == "123" test bypass issues + assert type(result) is type(expected) + + def test_allof_sequential_mutation_edge_case(self, caster_factory): + """ + Documents the edge case that AnyCaster's allOf logic sequentially mutates the value. + The first schema casts "2" to an int (2). The second schema (number) + receives the int 2, casts it to float (2.0), and returns the float. + """ + spec = { + "allOf": [{"type": "integer"}, {"type": "number"}], + } + schema = SchemaPath.from_dict(spec) + value = "2" + + result = caster_factory(schema).cast(value) + + # "2" -> int(2) -> float(2.0) + assert result == 2.0 + assert type(result) is float From 79dc69eaace8f6d75957058f777aa156294d5401 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 5 Mar 2026 17:45:22 +0000 Subject: [PATCH 3/3] docs: add inline comments explaining casting edge cases for composite schemas --- openapi_core/casting/schemas/casters.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openapi_core/casting/schemas/casters.py b/openapi_core/casting/schemas/casters.py index 0e835d41..2a0fd8e8 100644 --- a/openapi_core/casting/schemas/casters.py +++ b/openapi_core/casting/schemas/casters.py @@ -45,6 +45,9 @@ def cast(self, value: Any) -> Any: if "allOf" in self.schema: for subschema in self.schema / "allOf": try: + # Note: Mutates `value` iteratively. This sequentially + # resolves standard overlapping types but can cause edge cases + # if a string is casted to an int and passed to a string schema. value = self.schema_caster.evolve(subschema).cast(value) except (ValueError, TypeError, CastError): pass @@ -52,6 +55,8 @@ def cast(self, value: Any) -> Any: if "oneOf" in self.schema: for subschema in self.schema / "oneOf": try: + # Note: Greedy resolution. Will return the first successful + # cast based on the order of the oneOf array. return self.schema_caster.evolve(subschema).cast(value) except (ValueError, TypeError, CastError): pass @@ -59,6 +64,8 @@ def cast(self, value: Any) -> Any: if "anyOf" in self.schema: for subschema in self.schema / "anyOf": try: + # Note: Greedy resolution. Will return the first successful + # cast based on the order of the anyOf array. return self.schema_caster.evolve(subschema).cast(value) except (ValueError, TypeError, CastError): pass