Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions openapi_core/casting/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions openapi_core/casting/schemas/casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,39 @@ 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:
# 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

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

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

return value


PrimitiveType = TypeVar("PrimitiveType")


Expand Down
66 changes: 66 additions & 0 deletions tests/unit/casting/test_schema_casters.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,69 @@ 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

@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
Loading