diff --git a/docs/customizations.rst b/docs/customizations.rst index 92a94512..f215895b 100644 --- a/docs/customizations.rst +++ b/docs/customizations.rst @@ -21,7 +21,9 @@ If you know you have a valid specification already, disabling the validator can Media type deserializers ------------------------ -Pass custom defined media type deserializers dictionary with supported mimetypes as a key to `unmarshal_response` function: +OpenAPI comes with a set of built-in media type deserializers such as: ``application/json``, ``application/xml``, ``application/x-www-form-urlencoded`` or ``multipart/form-data``. + +You can also define your own ones. Pass custom defined media type deserializers dictionary with supported mimetypes as a key to `unmarshal_response` function: .. code-block:: python :emphasize-lines: 13 diff --git a/openapi_core/deserializing/media_types/__init__.py b/openapi_core/deserializing/media_types/__init__.py index b8aef87f..70331f9b 100644 --- a/openapi_core/deserializing/media_types/__init__.py +++ b/openapi_core/deserializing/media_types/__init__.py @@ -1,4 +1,5 @@ -from json import loads +from json import loads as json_loads +from xml.etree.ElementTree import fromstring as xml_loads from openapi_core.deserializing.media_types.datatypes import ( MediaTypeDeserializersDict, @@ -7,12 +8,18 @@ MediaTypeDeserializersFactory, ) from openapi_core.deserializing.media_types.util import data_form_loads +from openapi_core.deserializing.media_types.util import plain_loads from openapi_core.deserializing.media_types.util import urlencoded_form_loads __all__ = ["media_type_deserializers_factory"] media_type_deserializers: MediaTypeDeserializersDict = { - "application/json": loads, + "text/html": plain_loads, + "text/plain": plain_loads, + "application/json": json_loads, + "application/vnd.api+json": json_loads, + "application/xml": xml_loads, + "application/xhtml+xml": xml_loads, "application/x-www-form-urlencoded": urlencoded_form_loads, "multipart/form-data": data_form_loads, } diff --git a/openapi_core/deserializing/media_types/deserializers.py b/openapi_core/deserializing/media_types/deserializers.py index 4ba040cf..43f99c81 100644 --- a/openapi_core/deserializing/media_types/deserializers.py +++ b/openapi_core/deserializing/media_types/deserializers.py @@ -1,6 +1,7 @@ import warnings from typing import Any from typing import Optional +from xml.etree.ElementTree import ParseError from openapi_core.deserializing.media_types.datatypes import ( DeserializerCallable, @@ -26,5 +27,5 @@ def deserialize(self, value: Any) -> Any: try: return self.deserializer_callable(value) - except (ValueError, TypeError, AttributeError): + except (ParseError, ValueError, TypeError, AttributeError): raise MediaTypeDeserializeError(self.mimetype, value) diff --git a/openapi_core/deserializing/media_types/util.py b/openapi_core/deserializing/media_types/util.py index 4179cad0..df03eba2 100644 --- a/openapi_core/deserializing/media_types/util.py +++ b/openapi_core/deserializing/media_types/util.py @@ -5,6 +5,12 @@ from urllib.parse import parse_qsl +def plain_loads(value: Union[str, bytes]) -> str: + if isinstance(value, bytes): + value = value.decode("ASCII", errors="surrogateescape") + return value + + def urlencoded_form_loads(value: Any) -> Dict[str, Any]: return dict(parse_qsl(value)) diff --git a/tests/integration/test_petstore.py b/tests/integration/test_petstore.py index d821fdec..6a7055d1 100644 --- a/tests/integration/test_petstore.py +++ b/tests/integration/test_petstore.py @@ -233,8 +233,7 @@ def test_get_pets_response_no_schema(self, spec): data = "" response = MockResponse(data, status_code=404, mimetype="text/html") - with pytest.warns(UserWarning): - response_result = unmarshal_response(request, response, spec=spec) + response_result = unmarshal_response(request, response, spec=spec) assert response_result.errors == [] assert response_result.data == data diff --git a/tests/integration/unmarshalling/test_request_unmarshaller.py b/tests/integration/unmarshalling/test_request_unmarshaller.py index 31744659..62f6ba34 100644 --- a/tests/integration/unmarshalling/test_request_unmarshaller.py +++ b/tests/integration/unmarshalling/test_request_unmarshaller.py @@ -352,8 +352,7 @@ def test_post_pets_plain_no_schema(self, request_unmarshaller): mimetype="text/plain", ) - with pytest.warns(UserWarning): - result = request_unmarshaller.unmarshal(request) + result = request_unmarshaller.unmarshal(request) assert result.errors == [] assert result.parameters == Parameters( diff --git a/tests/unit/deserializing/test_media_types_deserializers.py b/tests/unit/deserializing/test_media_types_deserializers.py index 528996e5..e6f3bed8 100644 --- a/tests/unit/deserializing/test_media_types_deserializers.py +++ b/tests/unit/deserializing/test_media_types_deserializers.py @@ -1,3 +1,5 @@ +from xml.etree.ElementTree import Element + import pytest from openapi_core.deserializing.exceptions import DeserializeError @@ -46,16 +48,43 @@ def test_no_deserializer(self, deserializer_factory): assert result == value - def test_json_empty(self, deserializer_factory): - mimetype = "application/json" + @pytest.mark.parametrize( + "mimetype", + [ + "text/plain", + "text/html", + ], + ) + def test_plain_valid(self, deserializer_factory, mimetype): + deserializer = deserializer_factory(mimetype) + value = "somestr" + + result = deserializer.deserialize(value) + + assert result == value + + @pytest.mark.parametrize( + "mimetype", + [ + "application/json", + "application/vnd.api+json", + ], + ) + def test_json_empty(self, deserializer_factory, mimetype): deserializer = deserializer_factory(mimetype) value = "" with pytest.raises(DeserializeError): deserializer.deserialize(value) - def test_json_empty_object(self, deserializer_factory): - mimetype = "application/json" + @pytest.mark.parametrize( + "mimetype", + [ + "application/json", + "application/vnd.api+json", + ], + ) + def test_json_empty_object(self, deserializer_factory, mimetype): deserializer = deserializer_factory(mimetype) value = "{}" @@ -63,6 +92,35 @@ def test_json_empty_object(self, deserializer_factory): assert result == {} + @pytest.mark.parametrize( + "mimetype", + [ + "application/xml", + "application/xhtml+xml", + ], + ) + def test_xml_empty(self, deserializer_factory, mimetype): + deserializer = deserializer_factory(mimetype) + value = "" + + with pytest.raises(DeserializeError): + deserializer.deserialize(value) + + @pytest.mark.parametrize( + "mimetype", + [ + "application/xml", + "application/xhtml+xml", + ], + ) + def test_xml_valid(self, deserializer_factory, mimetype): + deserializer = deserializer_factory(mimetype) + value = "text" + + result = deserializer.deserialize(value) + + assert type(result) is Element + def test_urlencoded_form_empty(self, deserializer_factory): mimetype = "application/x-www-form-urlencoded" deserializer = deserializer_factory(mimetype)