diff --git a/openapi_core/casting/schemas/casters.py b/openapi_core/casting/schemas/casters.py index 94df492b..6bbb3374 100644 --- a/openapi_core/casting/schemas/casters.py +++ b/openapi_core/casting/schemas/casters.py @@ -1,7 +1,6 @@ from typing import Any from typing import Generic from typing import Iterable -from typing import List from typing import Mapping from typing import Optional from typing import Type @@ -28,6 +27,14 @@ def __init__( self.schema_caster = schema_caster def __call__(self, value: Any) -> Any: + self.validate(value) + + return self.cast(value) + + def validate(self, value: Any) -> None: + pass + + def cast(self, value: Any) -> Any: return value @@ -37,18 +44,9 @@ def __call__(self, value: Any) -> Any: class PrimitiveTypeCaster(Generic[PrimitiveType], PrimitiveCaster): primitive_type: Type[PrimitiveType] = NotImplemented - def __call__(self, value: Union[str, bytes]) -> Any: - self.validate(value) - + def cast(self, value: Union[str, bytes]) -> PrimitiveType: return self.primitive_type(value) # type: ignore [call-arg] - def validate(self, value: Any) -> None: - # FIXME: don't cast data from media type deserializer - # See https://github.com/python-openapi/openapi-core/issues/706 - # if not isinstance(value, (str, bytes)): - # raise ValueError("should cast only from string or bytes") - pass - class IntegerCaster(PrimitiveTypeCaster[int]): primitive_type = int @@ -61,22 +59,18 @@ class NumberCaster(PrimitiveTypeCaster[float]): class BooleanCaster(PrimitiveTypeCaster[bool]): primitive_type = bool - def __call__(self, value: Union[str, bytes]) -> Any: - self.validate(value) - - return self.primitive_type(forcebool(value)) - def validate(self, value: Any) -> None: super().validate(value) - # FIXME: don't cast data from media type deserializer - # See https://github.com/python-openapi/openapi-core/issues/706 if isinstance(value, bool): return if value.lower() not in ["false", "true"]: raise ValueError("not a boolean format") + def cast(self, value: Union[str, bytes]) -> bool: + return self.primitive_type(forcebool(value)) + class ArrayCaster(PrimitiveCaster): @property @@ -85,19 +79,21 @@ def items_caster(self) -> "SchemaCaster": items_schema = self.schema.get("items", SchemaPath.from_dict({})) return self.schema_caster.evolve(items_schema) - def __call__(self, value: Any) -> List[Any]: + def validate(self, value: Any) -> None: # str and bytes are not arrays according to the OpenAPI spec if isinstance(value, (str, bytes)) or not isinstance(value, Iterable): - raise CastError(value, self.schema["type"]) + raise ValueError("not an array format") - try: - return list(map(self.items_caster.cast, value)) - except (ValueError, TypeError): - raise CastError(value, self.schema["type"]) + def cast(self, value: list[Any]) -> list[Any]: + return list(map(self.items_caster.cast, value)) class ObjectCaster(PrimitiveCaster): - def __call__(self, value: Any) -> Any: + def validate(self, value: Any) -> None: + if not isinstance(value, dict): + raise ValueError("not an object format") + + def cast(self, value: dict[str, Any]) -> dict[str, Any]: return self._cast_proparties(value) def evolve(self, schema: SchemaPath) -> "ObjectCaster": @@ -109,9 +105,11 @@ def evolve(self, schema: SchemaPath) -> "ObjectCaster": self.schema_caster.evolve(schema), ) - def _cast_proparties(self, value: Any, schema_only: bool = False) -> Any: + def _cast_proparties( + self, value: dict[str, Any], schema_only: bool = False + ) -> dict[str, Any]: if not isinstance(value, dict): - raise CastError(value, self.schema["type"]) + raise ValueError("not an object format") all_of_schemas = self.schema_validator.iter_all_of_schemas(value) for all_of_schema in all_of_schemas: diff --git a/openapi_core/casting/schemas/datatypes.py b/openapi_core/casting/schemas/datatypes.py deleted file mode 100644 index 1014bf63..00000000 --- a/openapi_core/casting/schemas/datatypes.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Any -from typing import Callable - -CasterCallable = Callable[[Any], Any] diff --git a/openapi_core/casting/schemas/exceptions.py b/openapi_core/casting/schemas/exceptions.py index 242288d2..6e9749c0 100644 --- a/openapi_core/casting/schemas/exceptions.py +++ b/openapi_core/casting/schemas/exceptions.py @@ -1,11 +1,11 @@ from dataclasses import dataclass from typing import Any -from openapi_core.exceptions import OpenAPIError +from openapi_core.deserializing.exceptions import DeserializeError @dataclass -class CastError(OpenAPIError): +class CastError(DeserializeError): """Schema cast operation error""" value: Any diff --git a/openapi_core/deserializing/media_types/__init__.py b/openapi_core/deserializing/media_types/__init__.py index fd4a0ae1..dfcd4b9d 100644 --- a/openapi_core/deserializing/media_types/__init__.py +++ b/openapi_core/deserializing/media_types/__init__.py @@ -12,9 +12,8 @@ from openapi_core.deserializing.media_types.util import plain_loads from openapi_core.deserializing.media_types.util import urlencoded_form_loads from openapi_core.deserializing.media_types.util import xml_loads -from openapi_core.deserializing.styles import style_deserializers_factory -__all__ = ["media_type_deserializers_factory"] +__all__ = ["media_type_deserializers", "MediaTypeDeserializersFactory"] media_type_deserializers: MediaTypeDeserializersDict = defaultdict( lambda: binary_loads, @@ -30,8 +29,3 @@ "multipart/form-data": data_form_loads, } ) - -media_type_deserializers_factory = MediaTypeDeserializersFactory( - style_deserializers_factory, - media_type_deserializers=media_type_deserializers, -) diff --git a/openapi_core/deserializing/media_types/factories.py b/openapi_core/deserializing/media_types/factories.py index 45bc5075..2889923d 100644 --- a/openapi_core/deserializing/media_types/factories.py +++ b/openapi_core/deserializing/media_types/factories.py @@ -3,6 +3,7 @@ from jsonschema_path import SchemaPath +from openapi_core.casting.schemas.factories import SchemaCastersFactory from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) @@ -12,6 +13,7 @@ from openapi_core.deserializing.media_types.deserializers import ( MediaTypesDeserializer, ) +from openapi_core.deserializing.styles.datatypes import StyleDeserializersDict from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -28,6 +30,31 @@ def __init__( media_type_deserializers = {} self.media_type_deserializers = media_type_deserializers + @classmethod + def from_schema_casters_factory( + cls, + schema_casters_factory: SchemaCastersFactory, + style_deserializers: Optional[StyleDeserializersDict] = None, + media_type_deserializers: Optional[MediaTypeDeserializersDict] = None, + ) -> "MediaTypeDeserializersFactory": + from openapi_core.deserializing.media_types import ( + media_type_deserializers as default_media_type_deserializers, + ) + from openapi_core.deserializing.styles import ( + style_deserializers as default_style_deserializers, + ) + + style_deserializers_factory = StyleDeserializersFactory( + schema_casters_factory, + style_deserializers=style_deserializers + or default_style_deserializers, + ) + return cls( + style_deserializers_factory, + media_type_deserializers=media_type_deserializers + or default_media_type_deserializers, + ) + def create( self, mimetype: str, diff --git a/openapi_core/deserializing/styles/__init__.py b/openapi_core/deserializing/styles/__init__.py index f9ecef06..b7819950 100644 --- a/openapi_core/deserializing/styles/__init__.py +++ b/openapi_core/deserializing/styles/__init__.py @@ -10,7 +10,7 @@ from openapi_core.deserializing.styles.util import simple_loads from openapi_core.deserializing.styles.util import space_delimited_loads -__all__ = ["style_deserializers_factory"] +__all__ = ["style_deserializers", "StyleDeserializersFactory"] style_deserializers: StyleDeserializersDict = { "matrix": matrix_loads, @@ -21,7 +21,3 @@ "pipeDelimited": pipe_delimited_loads, "deepObject": deep_object_loads, } - -style_deserializers_factory = StyleDeserializersFactory( - style_deserializers=style_deserializers, -) diff --git a/openapi_core/deserializing/styles/deserializers.py b/openapi_core/deserializing/styles/deserializers.py index 2303f7a3..67770147 100644 --- a/openapi_core/deserializing/styles/deserializers.py +++ b/openapi_core/deserializing/styles/deserializers.py @@ -3,6 +3,10 @@ from typing import Mapping from typing import Optional +from jsonschema_path import SchemaPath + +from openapi_core.casting.schemas.casters import SchemaCaster +from openapi_core.casting.schemas.exceptions import CastError from openapi_core.deserializing.exceptions import DeserializeError from openapi_core.deserializing.styles.datatypes import DeserializerCallable @@ -13,13 +17,16 @@ def __init__( style: str, explode: bool, name: str, - schema_type: str, + schema: SchemaPath, + caster: SchemaCaster, deserializer_callable: Optional[DeserializerCallable] = None, ): self.style = style self.explode = explode self.name = name - self.schema_type = schema_type + self.schema = schema + self.schema_type = schema.getkey("type", "") + self.caster = caster self.deserializer_callable = deserializer_callable def deserialize(self, location: Mapping[str, Any]) -> Any: @@ -28,8 +35,13 @@ def deserialize(self, location: Mapping[str, Any]) -> Any: return location[self.name] try: - return self.deserializer_callable( + value = self.deserializer_callable( self.explode, self.name, self.schema_type, location ) - except (ValueError, TypeError, AttributeError): - raise DeserializeError(self.style, self.name) + except (ValueError, TypeError, AttributeError) as exc: + raise DeserializeError(self.style, self.name) from exc + + try: + return self.caster.cast(value) + except (ValueError, TypeError, AttributeError) as exc: + raise CastError(value, self.schema_type) from exc diff --git a/openapi_core/deserializing/styles/factories.py b/openapi_core/deserializing/styles/factories.py index 5758d97d..d184cd3a 100644 --- a/openapi_core/deserializing/styles/factories.py +++ b/openapi_core/deserializing/styles/factories.py @@ -2,6 +2,7 @@ from jsonschema_path import SchemaPath +from openapi_core.casting.schemas.factories import SchemaCastersFactory from openapi_core.deserializing.styles.datatypes import StyleDeserializersDict from openapi_core.deserializing.styles.deserializers import StyleDeserializer @@ -9,8 +10,10 @@ class StyleDeserializersFactory: def __init__( self, + schema_casters_factory: SchemaCastersFactory, style_deserializers: Optional[StyleDeserializersDict] = None, ): + self.schema_casters_factory = schema_casters_factory if style_deserializers is None: style_deserializers = {} self.style_deserializers = style_deserializers @@ -22,9 +25,8 @@ def create( schema: SchemaPath, name: str, ) -> StyleDeserializer: - schema_type = schema.getkey("type", "") - deserialize_callable = self.style_deserializers.get(style) + caster = self.schema_casters_factory.create(schema) return StyleDeserializer( - style, explode, name, schema_type, deserialize_callable + style, explode, name, schema, caster, deserialize_callable ) diff --git a/openapi_core/unmarshalling/request/protocols.py b/openapi_core/unmarshalling/request/protocols.py index 43a18cbe..6df7815e 100644 --- a/openapi_core/unmarshalling/request/protocols.py +++ b/openapi_core/unmarshalling/request/protocols.py @@ -8,16 +8,12 @@ from openapi_spec_validator.validation.types import SpecValidatorType from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -43,8 +39,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, @@ -74,8 +74,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, diff --git a/openapi_core/unmarshalling/request/unmarshallers.py b/openapi_core/unmarshalling/request/unmarshallers.py index efd45930..011dad81 100644 --- a/openapi_core/unmarshalling/request/unmarshallers.py +++ b/openapi_core/unmarshalling/request/unmarshallers.py @@ -4,16 +4,12 @@ from openapi_spec_validator.validation.types import SpecValidatorType from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -85,8 +81,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, diff --git a/openapi_core/unmarshalling/response/protocols.py b/openapi_core/unmarshalling/response/protocols.py index de90c58d..c68001e5 100644 --- a/openapi_core/unmarshalling/response/protocols.py +++ b/openapi_core/unmarshalling/response/protocols.py @@ -8,16 +8,12 @@ from openapi_spec_validator.validation.types import SpecValidatorType from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -44,8 +40,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, @@ -75,8 +75,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, diff --git a/openapi_core/unmarshalling/unmarshallers.py b/openapi_core/unmarshalling/unmarshallers.py index 984b9ea1..9d970e68 100644 --- a/openapi_core/unmarshalling/unmarshallers.py +++ b/openapi_core/unmarshalling/unmarshallers.py @@ -7,16 +7,12 @@ from openapi_spec_validator.validation.types import SpecValidatorType from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -39,8 +35,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, diff --git a/openapi_core/validation/configurations.py b/openapi_core/validation/configurations.py index ebc32fc4..d7db3cec 100644 --- a/openapi_core/validation/configurations.py +++ b/openapi_core/validation/configurations.py @@ -2,16 +2,12 @@ from typing import Optional from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -53,12 +49,10 @@ class ValidatorConfig: path_finder_cls: Optional[PathFinderType] = None webhook_path_finder_cls: Optional[PathFinderType] = None - style_deserializers_factory: StyleDeserializersFactory = ( - style_deserializers_factory - ) - media_type_deserializers_factory: MediaTypeDeserializersFactory = ( - media_type_deserializers_factory - ) + style_deserializers_factory: Optional[StyleDeserializersFactory] = None + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None schema_casters_factory: Optional[SchemaCastersFactory] = None schema_validators_factory: Optional[SchemaValidatorsFactory] = None diff --git a/openapi_core/validation/request/protocols.py b/openapi_core/validation/request/protocols.py index 983864e2..56fae066 100644 --- a/openapi_core/validation/request/protocols.py +++ b/openapi_core/validation/request/protocols.py @@ -9,16 +9,12 @@ from openapi_spec_validator.validation.types import SpecValidatorType from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -37,8 +33,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, @@ -68,8 +68,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, diff --git a/openapi_core/validation/request/validators.py b/openapi_core/validation/request/validators.py index f2e1ae95..1e3e0421 100644 --- a/openapi_core/validation/request/validators.py +++ b/openapi_core/validation/request/validators.py @@ -16,16 +16,12 @@ from openapi_core.casting.schemas.factories import SchemaCastersFactory from openapi_core.datatypes import Parameters from openapi_core.datatypes import RequestParameters -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -71,8 +67,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, diff --git a/openapi_core/validation/response/protocols.py b/openapi_core/validation/response/protocols.py index f0f33dc6..7a8c8006 100644 --- a/openapi_core/validation/response/protocols.py +++ b/openapi_core/validation/response/protocols.py @@ -9,16 +9,12 @@ from openapi_spec_validator.validation.types import SpecValidatorType from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory from openapi_core.deserializing.styles.factories import ( StyleDeserializersFactory, ) @@ -36,8 +32,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, @@ -68,8 +68,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, diff --git a/openapi_core/validation/validators.py b/openapi_core/validation/validators.py index a627f8a0..c1a4d91b 100644 --- a/openapi_core/validation/validators.py +++ b/openapi_core/validation/validators.py @@ -12,16 +12,14 @@ from openapi_spec_validator.validation.types import SpecValidatorType from openapi_core.casting.schemas.factories import SchemaCastersFactory -from openapi_core.deserializing.media_types import ( - media_type_deserializers_factory, -) +from openapi_core.deserializing.media_types import media_type_deserializers from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, ) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory +from openapi_core.deserializing.styles import style_deserializers from openapi_core.deserializing.styles.exceptions import ( EmptyQueryParameterValue, ) @@ -51,8 +49,12 @@ def __init__( self, spec: SchemaPath, base_url: Optional[str] = None, - style_deserializers_factory: StyleDeserializersFactory = style_deserializers_factory, - media_type_deserializers_factory: MediaTypeDeserializersFactory = media_type_deserializers_factory, + style_deserializers_factory: Optional[ + StyleDeserializersFactory + ] = None, + media_type_deserializers_factory: Optional[ + MediaTypeDeserializersFactory + ] = None, schema_casters_factory: Optional[SchemaCastersFactory] = None, schema_validators_factory: Optional[SchemaValidatorsFactory] = None, path_finder_cls: Optional[PathFinderType] = None, @@ -71,9 +73,19 @@ def __init__( ) if self.schema_casters_factory is NotImplemented: raise NotImplementedError("schema_casters_factory is not assigned") - self.style_deserializers_factory = style_deserializers_factory + self.style_deserializers_factory = ( + style_deserializers_factory + or StyleDeserializersFactory( + self.schema_casters_factory, + style_deserializers=style_deserializers, + ) + ) self.media_type_deserializers_factory = ( media_type_deserializers_factory + or MediaTypeDeserializersFactory( + self.style_deserializers_factory, + media_type_deserializers=media_type_deserializers, + ) ) self.schema_validators_factory = ( schema_validators_factory or self.schema_validators_factory @@ -145,10 +157,6 @@ def _deserialise_style( ) return deserializer.deserialize(location) - def _cast(self, schema: SchemaPath, value: Any) -> Any: - caster = self.schema_casters_factory.create(schema) - return caster.cast(value) - def _validate_schema(self, schema: SchemaPath, value: Any) -> None: validator = self.schema_validators_factory.create( schema, @@ -217,8 +225,7 @@ def _get_simple_param_or_header( ): param_or_header_name = param_or_header["name"] raise EmptyQueryParameterValue(param_or_header_name) - casted = self._cast(schema, deserialised) - return casted, schema + return deserialised, schema def _get_complex_param_or_header( self, @@ -249,22 +256,18 @@ def _get_content_schema_value_and_schema( return deserialised, None schema = media_type / "schema" - # cast for urlencoded content - # FIXME: don't cast data from media type deserializer - # See https://github.com/python-openapi/openapi-core/issues/706 - casted = self._cast(schema, deserialised) - return casted, schema + return deserialised, schema def _get_content_and_schema( self, raw: bytes, content: SchemaPath, mimetype: Optional[str] = None ) -> Tuple[Any, Optional[SchemaPath]]: - casted, schema = self._get_content_schema_value_and_schema( + deserialised, schema = self._get_content_schema_value_and_schema( raw, content, mimetype ) if schema is None: - return casted, None - self._validate_schema(schema, casted) - return casted, schema + return deserialised, None + self._validate_schema(schema, deserialised) + return deserialised, schema def _get_media_type_value( self, diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index 58fbb760..fc59d3a8 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -1689,7 +1689,7 @@ def test_post_tags_wrong_property_type(self, spec): spec=spec, cls=V30RequestBodyValidator, ) - assert type(exc_info.value.__cause__) is CastError + assert type(exc_info.value.__cause__) is InvalidSchemaValue def test_post_tags_additional_properties(self, spec): host_url = "http://petstore.swagger.io/v1" diff --git a/tests/integration/validation/test_strict_json_validation.py b/tests/integration/validation/test_strict_json_validation.py new file mode 100644 index 00000000..939f57f8 --- /dev/null +++ b/tests/integration/validation/test_strict_json_validation.py @@ -0,0 +1,232 @@ +import json + +import pytest +from jsonschema_path import SchemaPath + +from openapi_core import V30RequestValidator +from openapi_core import V30ResponseValidator +from openapi_core.testing import MockRequest +from openapi_core.testing import MockResponse +from openapi_core.validation.request.exceptions import InvalidRequestBody +from openapi_core.validation.response.exceptions import InvalidData + + +def _spec_schema_path() -> SchemaPath: + spec_dict = { + "openapi": "3.0.3", + "info": {"title": "Strict JSON Validation", "version": "1.0.0"}, + "servers": [{"url": "http://example.com"}], + "paths": { + "/users": { + "post": { + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + }, + "application/problem+json": { + "schema": {"$ref": "#/components/schemas/User"} + }, + }, + }, + "responses": { + "204": {"description": "No content"}, + }, + }, + "put": { + "requestBody": { + "required": True, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + }, + "encoding": { + "age": {"contentType": "application/json"}, + }, + } + }, + }, + "responses": { + "204": {"description": "No content"}, + }, + }, + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + }, + } + } + }, + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "id": {"type": "string", "format": "uuid"}, + "username": {"type": "string"}, + "age": {"type": "integer"}, + }, + "required": ["id", "username", "age"], + } + } + }, + } + return SchemaPath.from_dict(spec_dict) + + +@pytest.mark.parametrize( + "content_type", + [ + "application/json", + "application/problem+json", + ], +) +def test_response_validator_strict_json_types(content_type: str) -> None: + spec = _spec_schema_path() + validator = V30ResponseValidator(spec) + + request = MockRequest("http://example.com", "get", "/users") + response_json = { + "id": "123e4567-e89b-12d3-a456-426614174000", + "username": "Test User", + "age": "30", + } + response = MockResponse( + json.dumps(response_json).encode("utf-8"), + status_code=200, + content_type=content_type, + ) + + with pytest.raises(InvalidData): + validator.validate(request, response) + + +@pytest.mark.parametrize( + "content_type", + [ + "application/json", + "application/problem+json", + ], +) +def test_request_validator_strict_json_types(content_type: str) -> None: + spec = _spec_schema_path() + validator = V30RequestValidator(spec) + + request_json = { + "id": "123e4567-e89b-12d3-a456-426614174000", + "username": "Test User", + "age": "30", + } + request = MockRequest( + "http://example.com", + "post", + "/users", + content_type=content_type, + data=json.dumps(request_json).encode("utf-8"), + ) + + with pytest.raises(InvalidRequestBody): + validator.validate(request) + + +def test_request_validator_urlencoded_json_part_strict() -> None: + spec = _spec_schema_path() + validator = V30RequestValidator(spec) + + # urlencoded field age is declared as application/json (via encoding) + # and contains a JSON string "30" (invalid for integer schema) + request = MockRequest( + "http://example.com", + "put", + "/users", + content_type="application/x-www-form-urlencoded", + data=( + b"id=123e4567-e89b-12d3-a456-426614174000&" + b"username=Test+User&" + b"age=%2230%22" + ), + ) + + with pytest.raises(InvalidRequestBody): + validator.validate(request) + + +def test_response_validator_strict_json_nested_types() -> None: + """Test that nested JSON structures (arrays, objects) remain strict.""" + spec_dict = { + "openapi": "3.0.3", + "info": {"title": "Nested JSON Test", "version": "1.0.0"}, + "servers": [{"url": "http://example.com"}], + "paths": { + "/data": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": {"type": "integer"}, + }, + "metadata": { + "type": "object", + "properties": { + "count": { + "type": "integer" + } + }, + }, + }, + } + } + }, + } + } + } + } + }, + } + spec = SchemaPath.from_dict(spec_dict) + validator = V30ResponseValidator(spec) + + request = MockRequest("http://example.com", "get", "/data") + + # Test nested array with string integers (should fail) + response_json = {"ids": ["10", "20", "30"], "metadata": {"count": 5}} + response = MockResponse( + json.dumps(response_json).encode("utf-8"), + status_code=200, + content_type="application/json", + ) + with pytest.raises(InvalidData): + validator.validate(request, response) + + # Test nested object with string integer (should fail) + response_json2 = {"ids": [10, 20, 30], "metadata": {"count": "5"}} + response2 = MockResponse( + json.dumps(response_json2).encode("utf-8"), + status_code=200, + content_type="application/json", + ) + with pytest.raises(InvalidData): + validator.validate(request, response2) diff --git a/tests/unit/deserializing/test_media_types_deserializers.py b/tests/unit/deserializing/test_media_types_deserializers.py index 53b43c35..8444af70 100644 --- a/tests/unit/deserializing/test_media_types_deserializers.py +++ b/tests/unit/deserializing/test_media_types_deserializers.py @@ -3,12 +3,14 @@ import pytest from jsonschema_path import SchemaPath +from openapi_core.casting.schemas import oas31_schema_casters_factory from openapi_core.deserializing.exceptions import DeserializeError -from openapi_core.deserializing.media_types import media_type_deserializers +from openapi_core.deserializing.media_types import ( + media_type_deserializers as default_media_type_deserializers, +) from openapi_core.deserializing.media_types.factories import ( MediaTypeDeserializersFactory, ) -from openapi_core.deserializing.styles import style_deserializers_factory class TestMediaTypeDeserializer: @@ -19,12 +21,13 @@ def create_deserializer( schema=None, encoding=None, parameters=None, - media_type_deserializers=media_type_deserializers, + media_type_deserializers=default_media_type_deserializers, extra_media_type_deserializers=None, ): - return MediaTypeDeserializersFactory( - style_deserializers_factory, - media_type_deserializers, + + return MediaTypeDeserializersFactory.from_schema_casters_factory( + oas31_schema_casters_factory, + media_type_deserializers=media_type_deserializers, ).create( mimetype, schema=schema, @@ -247,7 +250,7 @@ def test_urlencoded_form_simple(self, deserializer_factory): "name": "foo bar", } - def test_urlencoded_complex(self, deserializer_factory): + def test_urlencoded_complex_cast_error(self, deserializer_factory): mimetype = "application/x-www-form-urlencoded" schema_dict = { "type": "object", @@ -264,10 +267,30 @@ def test_urlencoded_complex(self, deserializer_factory): deserializer = deserializer_factory(mimetype, schema=schema) value = b"prop=a&prop=b&prop=c" + with pytest.raises(DeserializeError): + deserializer.deserialize(value) + + def test_urlencoded_complex(self, deserializer_factory): + mimetype = "application/x-www-form-urlencoded" + schema_dict = { + "type": "object", + "properties": { + "prop": { + "type": "array", + "items": { + "type": "integer", + }, + }, + }, + } + schema = SchemaPath.from_dict(schema_dict) + deserializer = deserializer_factory(mimetype, schema=schema) + value = b"prop=1&prop=2&prop=3" + result = deserializer.deserialize(value) assert result == { - "prop": ["a", "b", "c"], + "prop": [1, 2, 3], } def test_urlencoded_content_type(self, deserializer_factory): @@ -339,9 +362,9 @@ def test_urlencoded_deepobject(self, deserializer_factory): assert result == { "color": { - "R": "100", - "G": "200", - "B": "150", + "R": 100, + "G": 200, + "B": 150, }, } diff --git a/tests/unit/deserializing/test_styles_deserializers.py b/tests/unit/deserializing/test_styles_deserializers.py index 29e52d25..8a3d4142 100644 --- a/tests/unit/deserializing/test_styles_deserializers.py +++ b/tests/unit/deserializing/test_styles_deserializers.py @@ -2,14 +2,23 @@ from jsonschema_path import SchemaPath from werkzeug.datastructures import ImmutableMultiDict +from openapi_core.casting.schemas import oas31_schema_casters_factory from openapi_core.deserializing.exceptions import DeserializeError -from openapi_core.deserializing.styles import style_deserializers_factory +from openapi_core.deserializing.styles import style_deserializers +from openapi_core.deserializing.styles.factories import ( + StyleDeserializersFactory, +) from openapi_core.schema.parameters import get_style_and_explode class TestParameterStyleDeserializer: @pytest.fixture def deserializer_factory(self): + style_deserializers_factory = StyleDeserializersFactory( + oas31_schema_casters_factory, + style_deserializers=style_deserializers, + ) + def create_deserializer(param, name=None): name = name or param["name"] style, explode = get_style_and_explode(param)