From 343b147b1d8a88174231e51d10e6634d7be49e7d Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Wed, 8 Jul 2026 21:20:13 +0530 Subject: [PATCH] Cast form values when matching allOf subschemas For application/x-www-form-urlencoded requests, iter_any_of_schemas casts scalar form values (which always arrive as strings) before validating each subschema, but iter_all_of_schemas did not. An allOf part containing a non-string field (boolean/integer/object) therefore failed validation against the raw string value and was silently dropped (only a 'invalid allOf schema found' warning), losing every field in that part. Give iter_all_of_schemas the same optional caster as iter_any_of_schemas and pass it from the media-type deserializer. Fixes #1212. --- .../media_types/deserializers.py | 6 ++- openapi_core/validation/schemas/validators.py | 19 +++++++++- .../test_media_types_deserializers.py | 38 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/openapi_core/deserializing/media_types/deserializers.py b/openapi_core/deserializing/media_types/deserializers.py index 1bf4b69e..59ec87a2 100644 --- a/openapi_core/deserializing/media_types/deserializers.py +++ b/openapi_core/deserializing/media_types/deserializers.py @@ -379,7 +379,11 @@ def iter_composed_all_of_schemas( assert self.schema_validator is not None if not self.mimetype.startswith("multipart"): - return list(self.schema_validator.iter_all_of_schemas(location)) + return list( + self.schema_validator.iter_all_of_schemas( + location, caster=self.schema_caster + ) + ) if self.schema is None or "allOf" not in self.schema: return [] diff --git a/openapi_core/validation/schemas/validators.py b/openapi_core/validation/schemas/validators.py index 87133119..b2701bb0 100644 --- a/openapi_core/validation/schemas/validators.py +++ b/openapi_core/validation/schemas/validators.py @@ -428,6 +428,7 @@ def iter_any_of_schemas( def iter_all_of_schemas( self, value: Any, + caster: Optional["SchemaCaster"] = None, ) -> Iterator[SchemaPath]: if "allOf" not in self.schema: return @@ -438,7 +439,23 @@ def iter_all_of_schemas( continue validator = self.evolve(subschema) try: - validator.validate(value) + test_value = value + # Only cast if caster provided (opt-in behavior). Useful for + # form-encoded data where scalar values arrive as strings and + # need casting before validation against non-string schemas. + if caster is not None: + try: + # Convert to dict if it's not exactly a plain dict + if type(value) is not dict: + test_value = dict(value) + else: + test_value = value + test_value = caster.evolve(subschema).cast(test_value) + except (ValueError, TypeError, Exception): + # If casting fails, validate with the original value + test_value = value + + validator.validate(test_value) except ValidateError: log.warning("invalid allOf schema found") else: diff --git a/tests/unit/deserializing/test_media_types_deserializers.py b/tests/unit/deserializing/test_media_types_deserializers.py index 05b1e532..7d9b9b33 100644 --- a/tests/unit/deserializing/test_media_types_deserializers.py +++ b/tests/unit/deserializing/test_media_types_deserializers.py @@ -564,6 +564,44 @@ def test_urlencoded_oneof_string_field(self, spec, deserializer_factory): "fieldA": "value", } + def test_urlencoded_allof_non_string_field( + self, spec, deserializer_factory + ): + """Test issue #1212: allOf with urlencoded must not drop a subschema + that has a non-string (e.g. boolean) field.""" + mimetype = "application/x-www-form-urlencoded" + schema_dict = { + "allOf": [ + { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "label": {"type": "string"}, + }, + }, + { + "type": "object", + "properties": {"name": {"type": "string"}}, + }, + ] + } + schema = SchemaPath.from_dict(schema_dict) + schema_validator = oas31_schema_validators_factory.create( + spec, schema + ) + deserializer = deserializer_factory( + mimetype, schema=schema, schema_validator=schema_validator + ) + value = b"name=widget&label=hello&enabled=true" + + result = deserializer.deserialize(value) + + assert result == { + "name": "widget", + "label": "hello", + "enabled": True, + } + def test_urlencoded_anyof_with_types(self, spec, deserializer_factory): """Test anyOf with urlencoded and type coercion""" mimetype = "application/x-www-form-urlencoded"