import datetime as dt import decimal import ipaddress import math import uuid from unittest.mock import patch import pytest from marshmallow import EXCLUDE, INCLUDE, RAISE, Schema, fields, validate from marshmallow.exceptions import ValidationError from marshmallow.validate import Equal from marshmallow.warnings import ( ChangedInMarshmallow4Warning, RemovedInMarshmallow4Warning, ) from tests.base import ( ALL_FIELDS, DateEnum, GenderEnum, HairColorEnum, assert_date_equal, assert_time_equal, central, ) class MockDateTimeOverflowError(dt.datetime): """Used to simulate the possible OverflowError of datetime.fromtimestamp""" def fromtimestamp(self, *args, **kwargs): raise OverflowError() class MockDateTimeOSError(dt.datetime): """Used to simulate the possible OSError of datetime.fromtimestamp""" def fromtimestamp(self, *args, **kwargs): raise OSError() class TestDeserializingNone: @pytest.mark.parametrize("FieldClass", ALL_FIELDS) def test_fields_allow_none_deserialize_to_none(self, FieldClass): field = FieldClass(allow_none=True) assert field.deserialize(None) is None # https://github.com/marshmallow-code/marshmallow/issues/111 @pytest.mark.parametrize("FieldClass", ALL_FIELDS) def test_fields_dont_allow_none_by_default(self, FieldClass): field = FieldClass() with pytest.raises(ValidationError, match="Field may not be null."): field.deserialize(None) def test_allow_none_is_true_if_missing_is_true(self): field = fields.Raw(load_default=None) assert field.allow_none is True assert field.deserialize(None) is None def test_list_field_deserialize_none_to_none(self): field = fields.List(fields.String(allow_none=True), allow_none=True) assert field.deserialize(None) is None def test_tuple_field_deserialize_none_to_none(self): field = fields.Tuple([fields.String()], allow_none=True) assert field.deserialize(None) is None def test_list_of_nested_allow_none_deserialize_none_to_none(self): field = fields.List(fields.Nested(Schema(), allow_none=True)) assert field.deserialize([None]) == [None] def test_list_of_nested_non_allow_none_deserialize_none_to_validation_error(self): field = fields.List(fields.Nested(Schema(), allow_none=False)) with pytest.raises(ValidationError): field.deserialize([None]) class TestFieldDeserialization: def test_float_field_deserialization(self): field = fields.Float() assert math.isclose(field.deserialize("12.3"), 12.3) assert math.isclose(field.deserialize(12.3), 12.3) @pytest.mark.parametrize("in_val", ["bad", "", {}, True, False]) def test_invalid_float_field_deserialization(self, in_val): field = fields.Float() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_val) assert excinfo.value.args[0] == "Not a valid number." def test_float_field_overflow(self): field = fields.Float() with pytest.raises(ValidationError) as excinfo: field.deserialize(2**1024) assert excinfo.value.args[0] == "Number too large." def test_integer_field_deserialization(self): field = fields.Integer() assert field.deserialize("42") == 42 with pytest.raises(ValidationError) as excinfo: field.deserialize("42.0") assert excinfo.value.args[0] == "Not a valid integer." with pytest.raises(ValidationError): field.deserialize("bad") assert excinfo.value.args[0] == "Not a valid integer." with pytest.raises(ValidationError): field.deserialize({}) assert excinfo.value.args[0] == "Not a valid integer." def test_strict_integer_field_deserialization(self): field = fields.Integer(strict=True) assert field.deserialize(42) == 42 with pytest.raises(ValidationError) as excinfo: field.deserialize(42.0) assert excinfo.value.args[0] == "Not a valid integer." with pytest.raises(ValidationError) as excinfo: field.deserialize(decimal.Decimal("42.0")) assert excinfo.value.args[0] == "Not a valid integer." with pytest.raises(ValidationError) as excinfo: field.deserialize("42") assert excinfo.value.args[0] == "Not a valid integer." def test_decimal_field_deserialization(self): m1 = 12 m2 = "12.355" m3 = decimal.Decimal(1) m4 = 3.14 m5 = "abc" m6 = [1, 2] field = fields.Decimal() assert isinstance(field.deserialize(m1), decimal.Decimal) assert field.deserialize(m1) == decimal.Decimal(12) assert isinstance(field.deserialize(m2), decimal.Decimal) assert field.deserialize(m2) == decimal.Decimal("12.355") assert isinstance(field.deserialize(m3), decimal.Decimal) assert field.deserialize(m3) == decimal.Decimal(1) assert isinstance(field.deserialize(m4), decimal.Decimal) assert field.deserialize(m4).as_tuple() == (0, (3, 1, 4), -2) with pytest.raises(ValidationError) as excinfo: field.deserialize(m5) assert excinfo.value.args[0] == "Not a valid number." with pytest.raises(ValidationError) as excinfo: field.deserialize(m6) assert excinfo.value.args[0] == "Not a valid number." def test_decimal_field_with_places(self): m1 = 12 m2 = "12.355" m3 = decimal.Decimal(1) m4 = "abc" m5 = [1, 2] field = fields.Decimal(1) assert isinstance(field.deserialize(m1), decimal.Decimal) assert field.deserialize(m1) == decimal.Decimal(12) assert isinstance(field.deserialize(m2), decimal.Decimal) assert field.deserialize(m2) == decimal.Decimal("12.4") assert isinstance(field.deserialize(m3), decimal.Decimal) assert field.deserialize(m3) == decimal.Decimal(1) with pytest.raises(ValidationError) as excinfo: field.deserialize(m4) assert excinfo.value.args[0] == "Not a valid number." with pytest.raises(ValidationError) as excinfo: field.deserialize(m5) assert excinfo.value.args[0] == "Not a valid number." def test_decimal_field_with_places_and_rounding(self): m1 = 12 m2 = "12.355" m3 = decimal.Decimal(1) m4 = "abc" m5 = [1, 2] field = fields.Decimal(1, decimal.ROUND_DOWN) assert isinstance(field.deserialize(m1), decimal.Decimal) assert field.deserialize(m1) == decimal.Decimal(12) assert isinstance(field.deserialize(m2), decimal.Decimal) assert field.deserialize(m2) == decimal.Decimal("12.3") assert isinstance(field.deserialize(m3), decimal.Decimal) assert field.deserialize(m3) == decimal.Decimal(1) with pytest.raises(ValidationError): field.deserialize(m4) with pytest.raises(ValidationError): field.deserialize(m5) def test_decimal_field_deserialization_string(self): m1 = 12 m2 = "12.355" m3 = decimal.Decimal(1) m4 = "abc" m5 = [1, 2] field = fields.Decimal(as_string=True) assert isinstance(field.deserialize(m1), decimal.Decimal) assert field.deserialize(m1) == decimal.Decimal(12) assert isinstance(field.deserialize(m2), decimal.Decimal) assert field.deserialize(m2) == decimal.Decimal("12.355") assert isinstance(field.deserialize(m3), decimal.Decimal) assert field.deserialize(m3) == decimal.Decimal(1) with pytest.raises(ValidationError): field.deserialize(m4) with pytest.raises(ValidationError): field.deserialize(m5) def test_decimal_field_special_values(self): m1 = "-NaN" m2 = "NaN" m3 = "-sNaN" m4 = "sNaN" m5 = "-Infinity" m6 = "Infinity" m7 = "-0" field = fields.Decimal(places=2, allow_nan=True) m1d = field.deserialize(m1) assert isinstance(m1d, decimal.Decimal) assert m1d.is_qnan() and not m1d.is_signed() m2d = field.deserialize(m2) assert isinstance(m2d, decimal.Decimal) assert m2d.is_qnan() and not m2d.is_signed() m3d = field.deserialize(m3) assert isinstance(m3d, decimal.Decimal) assert m3d.is_qnan() and not m3d.is_signed() m4d = field.deserialize(m4) assert isinstance(m4d, decimal.Decimal) assert m4d.is_qnan() and not m4d.is_signed() m5d = field.deserialize(m5) assert isinstance(m5d, decimal.Decimal) assert m5d.is_infinite() and m5d.is_signed() m6d = field.deserialize(m6) assert isinstance(m6d, decimal.Decimal) assert m6d.is_infinite() and not m6d.is_signed() m7d = field.deserialize(m7) assert isinstance(m7d, decimal.Decimal) assert m7d.is_zero() and m7d.is_signed() def test_decimal_field_special_values_not_permitted(self): m1 = "-NaN" m2 = "NaN" m3 = "-sNaN" m4 = "sNaN" m5 = "-Infinity" m6 = "Infinity" m7 = "-0" field = fields.Decimal(places=2) with pytest.raises(ValidationError) as excinfo: field.deserialize(m1) assert str(excinfo.value.args[0]) == ( "Special numeric values (nan or infinity) are not permitted." ) with pytest.raises(ValidationError): field.deserialize(m2) with pytest.raises(ValidationError): field.deserialize(m3) with pytest.raises(ValidationError): field.deserialize(m4) with pytest.raises(ValidationError): field.deserialize(m5) with pytest.raises(ValidationError): field.deserialize(m6) m7d = field.deserialize(m7) assert isinstance(m7d, decimal.Decimal) assert m7d.is_zero() and m7d.is_signed() @pytest.mark.parametrize("allow_nan", (None, False, True)) @pytest.mark.parametrize("value", ("nan", "-nan", "inf", "-inf")) def test_float_field_allow_nan(self, value, allow_nan): if allow_nan is None: # Test default case is False field = fields.Float() else: field = fields.Float(allow_nan=allow_nan) if allow_nan is True: res = field.deserialize(value) assert isinstance(res, float) if value.endswith("nan"): assert math.isnan(res) else: assert res == float(value) else: with pytest.raises(ValidationError) as excinfo: field.deserialize(value) assert str(excinfo.value.args[0]) == ( "Special numeric values (nan or infinity) are not permitted." ) def test_string_field_deserialization(self): field = fields.String() assert field.deserialize("foo") == "foo" assert field.deserialize(b"foo") == "foo" # https://github.com/marshmallow-code/marshmallow/issues/231 with pytest.raises(ValidationError) as excinfo: field.deserialize(42) assert excinfo.value.args[0] == "Not a valid string." with pytest.raises(ValidationError): field.deserialize({}) def test_boolean_field_deserialization(self): field = fields.Boolean() assert field.deserialize(True) is True assert field.deserialize(False) is False assert field.deserialize("True") is True assert field.deserialize("False") is False assert field.deserialize("true") is True assert field.deserialize("false") is False assert field.deserialize("1") is True assert field.deserialize("0") is False assert field.deserialize("on") is True assert field.deserialize("ON") is True assert field.deserialize("On") is True assert field.deserialize("off") is False assert field.deserialize("OFF") is False assert field.deserialize("Off") is False assert field.deserialize("y") is True assert field.deserialize("Y") is True assert field.deserialize("yes") is True assert field.deserialize("YES") is True assert field.deserialize("Yes") is True assert field.deserialize("n") is False assert field.deserialize("N") is False assert field.deserialize("no") is False assert field.deserialize("NO") is False assert field.deserialize("No") is False assert field.deserialize(1) is True assert field.deserialize(0) is False with pytest.raises(ValidationError) as excinfo: field.deserialize({}) assert excinfo.value.args[0] == "Not a valid boolean." with pytest.raises(ValidationError) as excinfo: field.deserialize(42) with pytest.raises(ValidationError) as excinfo: field.deserialize("invalid-string") def test_boolean_field_deserialization_with_custom_truthy_values(self): class MyBoolean(fields.Boolean): truthy = {"yep"} field = MyBoolean() assert field.deserialize("yep") is True field = fields.Boolean(truthy=("yep",)) assert field.deserialize("yep") is True assert field.deserialize(False) is False @pytest.mark.parametrize("in_val", ["notvalid", 123]) def test_boolean_field_deserialization_with_custom_truthy_values_invalid( self, in_val ): class MyBoolean(fields.Boolean): truthy = {"yep"} field = MyBoolean() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_val) expected_msg = "Not a valid boolean." assert str(excinfo.value.args[0]) == expected_msg field = fields.Boolean(truthy=("yep",)) with pytest.raises(ValidationError) as excinfo: field.deserialize(in_val) expected_msg = "Not a valid boolean." assert str(excinfo.value.args[0]) == expected_msg field2 = MyBoolean(error_messages={"invalid": "bad input"}) with pytest.raises(ValidationError) as excinfo: field2.deserialize(in_val) assert str(excinfo.value.args[0]) == "bad input" field2 = fields.Boolean( truthy=("yep",), error_messages={"invalid": "bad input"} ) def test_boolean_field_deserialization_with_empty_truthy(self): field = fields.Boolean(truthy=()) assert field.deserialize("yep") is True assert field.deserialize(True) is True assert field.deserialize(False) is False def test_boolean_field_deserialization_with_custom_falsy_values(self): field = fields.Boolean(falsy=("nope",)) assert field.deserialize("nope") is False assert field.deserialize(True) is True def test_field_toggle_show_invalid_value_in_error_message(self): error_messages = {"invalid": "Not valid: {input}"} boolfield = fields.Boolean(error_messages=error_messages) with pytest.raises(ValidationError) as excinfo: boolfield.deserialize("notabool") assert str(excinfo.value.args[0]) == "Not valid: notabool" with pytest.warns(ChangedInMarshmallow4Warning): numfield = fields.Number(error_messages=error_messages) with pytest.raises(ValidationError) as excinfo: numfield.deserialize("notanum") assert str(excinfo.value.args[0]) == "Not valid: notanum" intfield = fields.Integer(error_messages=error_messages) with pytest.raises(ValidationError) as excinfo: intfield.deserialize("notanint") assert str(excinfo.value.args[0]) == "Not valid: notanint" date_error_messages = {"invalid": "Not a valid {obj_type}: {input}"} datefield = fields.DateTime(error_messages=date_error_messages) with pytest.raises(ValidationError) as excinfo: datefield.deserialize("notadate") assert str(excinfo.value.args[0]) == "Not a valid datetime: notadate" @pytest.mark.parametrize( "in_value", [ "not-a-datetime", 42, True, False, 0, "", [], "2018", "2018-01-01", dt.datetime.now().strftime("%H:%M:%S %Y-%m-%d"), dt.datetime.now().strftime("%m-%d-%Y %H:%M:%S"), ], ) def test_invalid_datetime_deserialization(self, in_value): field = fields.DateTime() with pytest.raises(ValidationError, match="Not a valid datetime."): field.deserialize(in_value) def test_custom_date_format_datetime_field_deserialization(self): # Datetime string with format "%H:%M:%S.%f %Y-%m-%d" datestring = "10:11:12.123456 2019-01-02" # Deserialization should fail when datestring is not of same format field = fields.DateTime(format="%d-%m-%Y %H:%M:%S") with pytest.raises(ValidationError, match="Not a valid datetime."): field.deserialize(datestring) field = fields.DateTime(format="%H:%M:%S.%f %Y-%m-%d") assert field.deserialize(datestring) == dt.datetime( 2019, 1, 2, 10, 11, 12, 123456 ) field = fields.NaiveDateTime(format="%H:%M:%S.%f %Y-%m-%d") assert field.deserialize(datestring) == dt.datetime( 2019, 1, 2, 10, 11, 12, 123456 ) field = fields.AwareDateTime(format="%H:%M:%S.%f %Y-%m-%d") with pytest.raises(ValidationError, match="Not a valid aware datetime."): field.deserialize(datestring) @pytest.mark.parametrize("fmt", ["rfc", "rfc822"]) @pytest.mark.parametrize( ("value", "expected", "aware"), [ ( "Sun, 10 Nov 2013 01:23:45 -0000", dt.datetime(2013, 11, 10, 1, 23, 45), False, ), ( "Sun, 10 Nov 2013 01:23:45 +0000", dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=dt.timezone.utc), True, ), ( "Sun, 10 Nov 2013 01:23:45 -0600", dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central), True, ), ], ) def test_rfc_datetime_field_deserialization(self, fmt, value, expected, aware): field = fields.DateTime(format=fmt) assert field.deserialize(value) == expected field = fields.NaiveDateTime(format=fmt) if aware: with pytest.raises(ValidationError, match="Not a valid naive datetime."): field.deserialize(value) else: assert field.deserialize(value) == expected field = fields.AwareDateTime(format=fmt) if not aware: with pytest.raises(ValidationError, match="Not a valid aware datetime."): field.deserialize(value) else: assert field.deserialize(value) == expected @pytest.mark.parametrize("fmt", ["iso", "iso8601"]) @pytest.mark.parametrize( ("value", "expected", "aware"), [ ("2013-11-10T01:23:45", dt.datetime(2013, 11, 10, 1, 23, 45), False), ( "2013-11-10T01:23:45+00:00", dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=dt.timezone.utc), True, ), ( # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1251 "2013-11-10T01:23:45.123+00:00", dt.datetime(2013, 11, 10, 1, 23, 45, 123000, tzinfo=dt.timezone.utc), True, ), ( "2013-11-10T01:23:45.123456+00:00", dt.datetime(2013, 11, 10, 1, 23, 45, 123456, tzinfo=dt.timezone.utc), True, ), ( "2013-11-10T01:23:45-06:00", dt.datetime(2013, 11, 10, 1, 23, 45, tzinfo=central), True, ), ], ) def test_iso_datetime_field_deserialization(self, fmt, value, expected, aware): field = fields.DateTime(format=fmt) assert field.deserialize(value) == expected field = fields.NaiveDateTime(format=fmt) if aware: with pytest.raises(ValidationError, match="Not a valid naive datetime."): field.deserialize(value) else: assert field.deserialize(value) == expected field = fields.AwareDateTime(format=fmt) if not aware: with pytest.raises(ValidationError, match="Not a valid aware datetime."): field.deserialize(value) else: assert field.deserialize(value) == expected @pytest.mark.parametrize( ("fmt", "value", "expected"), [ ("timestamp", 1384043025, dt.datetime(2013, 11, 10, 0, 23, 45)), ("timestamp", "1384043025", dt.datetime(2013, 11, 10, 0, 23, 45)), ("timestamp", 1384043025, dt.datetime(2013, 11, 10, 0, 23, 45)), ("timestamp", 1384043025.12, dt.datetime(2013, 11, 10, 0, 23, 45, 120000)), ( "timestamp", 1384043025.123456, dt.datetime(2013, 11, 10, 0, 23, 45, 123456), ), ("timestamp", 1, dt.datetime(1970, 1, 1, 0, 0, 1)), ("timestamp_ms", 1384043025000, dt.datetime(2013, 11, 10, 0, 23, 45)), ("timestamp_ms", 1000, dt.datetime(1970, 1, 1, 0, 0, 1)), ], ) def test_timestamp_field_deserialization(self, fmt, value, expected): field = fields.DateTime(format=fmt) assert field.deserialize(value) == expected # By default, a datetime from a timestamp is never aware. field = fields.NaiveDateTime(format=fmt) assert field.deserialize(value) == expected field = fields.AwareDateTime(format=fmt) with pytest.raises(ValidationError, match="Not a valid aware datetime."): field.deserialize(value) # But it can be added by providing a default. field = fields.AwareDateTime(format=fmt, default_timezone=central) expected_aware = expected.replace(tzinfo=central) assert field.deserialize(value) == expected_aware @pytest.mark.parametrize("fmt", ["timestamp", "timestamp_ms"]) @pytest.mark.parametrize( "in_value", ["", "!@#", -1, dt.datetime(2013, 11, 10, 1, 23, 45)], ) def test_invalid_timestamp_field_deserialization(self, fmt, in_value): field = fields.DateTime(format=fmt) with pytest.raises(ValidationError, match="Not a valid datetime."): field.deserialize(in_value) #  Regression test for https://github.com/marshmallow-code/marshmallow/pull/2102 @pytest.mark.parametrize("fmt", ["timestamp", "timestamp_ms"]) @pytest.mark.parametrize( "mock_fromtimestamp", [MockDateTimeOSError, MockDateTimeOverflowError] ) def test_oversized_timestamp_field_deserialization(self, fmt, mock_fromtimestamp): with patch("datetime.datetime", mock_fromtimestamp): field = fields.DateTime(format=fmt) with pytest.raises(ValidationError, match="Not a valid datetime."): field.deserialize(99999999999999999) @pytest.mark.parametrize( ("fmt", "timezone", "value", "expected"), [ ("iso", None, "2013-11-10T01:23:45", dt.datetime(2013, 11, 10, 1, 23, 45)), ( "iso", dt.timezone.utc, "2013-11-10T01:23:45+00:00", dt.datetime(2013, 11, 10, 1, 23, 45), ), ( "iso", central, "2013-11-10T01:23:45-03:00", dt.datetime(2013, 11, 9, 22, 23, 45), ), ( "rfc", None, "Sun, 10 Nov 2013 01:23:45 -0000", dt.datetime(2013, 11, 10, 1, 23, 45), ), ( "rfc", dt.timezone.utc, "Sun, 10 Nov 2013 01:23:45 +0000", dt.datetime(2013, 11, 10, 1, 23, 45), ), ( "rfc", central, "Sun, 10 Nov 2013 01:23:45 -0300", dt.datetime(2013, 11, 9, 22, 23, 45), ), ], ) def test_naive_datetime_with_timezone(self, fmt, timezone, value, expected): field = fields.NaiveDateTime(format=fmt, timezone=timezone) assert field.deserialize(value) == expected @pytest.mark.parametrize("timezone", (dt.timezone.utc, central)) @pytest.mark.parametrize( ("fmt", "value"), [("iso", "2013-11-10T01:23:45"), ("rfc", "Sun, 10 Nov 2013 01:23:45")], ) def test_aware_datetime_default_timezone(self, fmt, timezone, value): field = fields.AwareDateTime(format=fmt, default_timezone=timezone) assert field.deserialize(value) == dt.datetime( 2013, 11, 10, 1, 23, 45, tzinfo=timezone ) def test_time_field_deserialization(self): field = fields.Time() t = dt.time(1, 23, 45) t_formatted = t.isoformat() result = field.deserialize(t_formatted) assert isinstance(result, dt.time) assert_time_equal(result, t) # With microseconds t2 = dt.time(1, 23, 45, 6789) t2_formatted = t2.isoformat() result2 = field.deserialize(t2_formatted) assert_time_equal(result2, t2) @pytest.mark.parametrize("in_data", ["badvalue", "", [], 42]) def test_invalid_time_field_deserialization(self, in_data): field = fields.Time() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_data) assert excinfo.value.args[0] == "Not a valid time." def test_custom_time_format_time_field_deserialization(self): # Time string with format "%f.%S:%M:%H" timestring = "123456.12:11:10" # Deserialization should fail when timestring is not of same format field = fields.Time(format="%S:%M:%H") with pytest.raises(ValidationError, match="Not a valid time."): field.deserialize(timestring) field = fields.Time(format="%f.%S:%M:%H") assert field.deserialize(timestring) == dt.time(10, 11, 12, 123456) @pytest.mark.parametrize("fmt", ["iso", "iso8601", None]) @pytest.mark.parametrize( ("value", "expected"), [ ("01:23:45", dt.time(1, 23, 45)), ("01:23:45+01:00", dt.time(1, 23, 45)), ("01:23:45.123", dt.time(1, 23, 45, 123000)), ("01:23:45.123456", dt.time(1, 23, 45, 123456)), ], ) def test_iso_time_field_deserialization(self, fmt, value, expected): if fmt is None: field = fields.Time() else: field = fields.Time(format=fmt) assert field.deserialize(value) == expected def test_invalid_timedelta_precision(self): with pytest.raises(ValueError, match='The precision must be "days",'): fields.TimeDelta("invalid") def test_timedelta_field_deserialization(self): field = fields.TimeDelta() result = field.deserialize("42") assert isinstance(result, dt.timedelta) assert result.days == 0 assert result.seconds == 42 assert result.microseconds == 0 field = fields.TimeDelta(fields.TimeDelta.SECONDS) result = field.deserialize(100000) assert result.days == 1 assert result.seconds == 13600 assert result.microseconds == 0 field = fields.TimeDelta(fields.TimeDelta.DAYS) result = field.deserialize("-42") assert isinstance(result, dt.timedelta) assert result.days == -42 assert result.seconds == 0 assert result.microseconds == 0 field = fields.TimeDelta(fields.TimeDelta.MICROSECONDS) result = field.deserialize(10**6 + 1) assert isinstance(result, dt.timedelta) assert result.days == 0 assert result.seconds == 1 assert result.microseconds == 1 field = fields.TimeDelta(fields.TimeDelta.MICROSECONDS) result = field.deserialize(86400 * 10**6 + 1) assert isinstance(result, dt.timedelta) assert result.days == 1 assert result.seconds == 0 assert result.microseconds == 1 field = fields.TimeDelta() result = field.deserialize(12.9) assert isinstance(result, dt.timedelta) assert result.days == 0 assert result.seconds == 12 assert result.microseconds == 0 field = fields.TimeDelta(fields.TimeDelta.WEEKS) result = field.deserialize(1) assert isinstance(result, dt.timedelta) assert result.days == 7 assert result.seconds == 0 assert result.microseconds == 0 field = fields.TimeDelta(fields.TimeDelta.HOURS) result = field.deserialize(25) assert isinstance(result, dt.timedelta) assert result.days == 1 assert result.seconds == 3600 assert result.microseconds == 0 field = fields.TimeDelta(fields.TimeDelta.MINUTES) result = field.deserialize(1441) assert isinstance(result, dt.timedelta) assert result.days == 1 assert result.seconds == 60 assert result.microseconds == 0 field = fields.TimeDelta(fields.TimeDelta.MILLISECONDS) result = field.deserialize(123456) assert isinstance(result, dt.timedelta) assert result.days == 0 assert result.seconds == 123 assert result.microseconds == 456000 total_microseconds_value = 322.0 field = fields.TimeDelta(fields.TimeDelta.MICROSECONDS, float) result = field.deserialize(total_microseconds_value) assert isinstance(result, dt.timedelta) unit_value = dt.timedelta(microseconds=1).total_seconds() assert math.isclose( result.total_seconds() / unit_value, total_microseconds_value ) total_microseconds_value = 322.12345 field = fields.TimeDelta(fields.TimeDelta.MICROSECONDS, float) result = field.deserialize(total_microseconds_value) assert isinstance(result, dt.timedelta) unit_value = dt.timedelta(microseconds=1).total_seconds() assert math.isclose( result.total_seconds() / unit_value, math.floor(total_microseconds_value) ) total_milliseconds_value = 322.223 field = fields.TimeDelta(fields.TimeDelta.MILLISECONDS, float) result = field.deserialize(total_milliseconds_value) assert isinstance(result, dt.timedelta) unit_value = dt.timedelta(milliseconds=1).total_seconds() assert math.isclose( result.total_seconds() / unit_value, total_milliseconds_value ) total_seconds_value = 322.223 field = fields.TimeDelta(fields.TimeDelta.SECONDS, float) result = field.deserialize(total_seconds_value) assert isinstance(result, dt.timedelta) assert math.isclose(result.total_seconds(), total_seconds_value) total_minutes_value = 322.223 field = fields.TimeDelta(fields.TimeDelta.MINUTES, float) result = field.deserialize(total_minutes_value) assert isinstance(result, dt.timedelta) unit_value = dt.timedelta(minutes=1).total_seconds() assert math.isclose(result.total_seconds() / unit_value, total_minutes_value) total_hours_value = 322.223 field = fields.TimeDelta(fields.TimeDelta.HOURS, float) result = field.deserialize(total_hours_value) assert isinstance(result, dt.timedelta) unit_value = dt.timedelta(hours=1).total_seconds() assert math.isclose(result.total_seconds() / unit_value, total_hours_value) total_days_value = 322.223 field = fields.TimeDelta(fields.TimeDelta.DAYS, float) result = field.deserialize(total_days_value) assert isinstance(result, dt.timedelta) unit_value = dt.timedelta(days=1).total_seconds() assert math.isclose(result.total_seconds() / unit_value, total_days_value) total_weeks_value = 322.223 field = fields.TimeDelta(fields.TimeDelta.WEEKS, float) result = field.deserialize(total_weeks_value) assert isinstance(result, dt.timedelta) unit_value = dt.timedelta(weeks=1).total_seconds() assert math.isclose(result.total_seconds() / unit_value, total_weeks_value) @pytest.mark.parametrize("in_value", ["", "badvalue", [], 9999999999]) def test_invalid_timedelta_field_deserialization(self, in_value): field = fields.TimeDelta(fields.TimeDelta.DAYS) with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) assert excinfo.value.args[0] == "Not a valid period of time." @pytest.mark.parametrize("format", (None, "%Y-%m-%d")) def test_date_field_deserialization(self, format): field = fields.Date(format=format) d = dt.date(2014, 8, 21) iso_date = d.isoformat() result = field.deserialize(iso_date) assert type(result) is dt.date assert_date_equal(result, d) @pytest.mark.parametrize( "in_value", ["", 123, [], dt.date(2014, 8, 21).strftime("%d-%m-%Y")] ) def test_invalid_date_field_deserialization(self, in_value): field = fields.Date() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) msg = "Not a valid date." assert excinfo.value.args[0] == msg def test_dict_field_deserialization(self): data = {"foo": "bar"} field = fields.Dict() load = field.deserialize(data) assert load == {"foo": "bar"} # Check load is a distinct object load["foo"] = "baz" assert data["foo"] == "bar" with pytest.raises(ValidationError) as excinfo: field.deserialize("baddict") assert excinfo.value.args[0] == "Not a valid mapping type." def test_structured_dict_value_deserialization(self): field = fields.Dict(values=fields.List(fields.Str)) assert field.deserialize({"foo": ["bar", "baz"]}) == {"foo": ["bar", "baz"]} with pytest.raises(ValidationError) as excinfo: field.deserialize({"foo": [1, 2], "bar": "baz", "ham": ["spam"]}) assert excinfo.value.args[0] == { "foo": {"value": {0: ["Not a valid string."], 1: ["Not a valid string."]}}, "bar": {"value": ["Not a valid list."]}, } assert excinfo.value.valid_data == {"foo": [], "ham": ["spam"]} def test_structured_dict_key_deserialization(self): field = fields.Dict(keys=fields.Str) assert field.deserialize({"foo": "bar"}) == {"foo": "bar"} with pytest.raises(ValidationError) as excinfo: field.deserialize({1: "bar", "foo": "baz"}) assert excinfo.value.args[0] == {1: {"key": ["Not a valid string."]}} assert excinfo.value.valid_data == {"foo": "baz"} def test_structured_dict_key_value_deserialization(self): field = fields.Dict( keys=fields.Str( validate=[validate.Email(), validate.Regexp(r".*@test\.com$")] ), values=fields.Decimal, ) assert field.deserialize({"foo@test.com": 1}) == { "foo@test.com": decimal.Decimal(1) } with pytest.raises(ValidationError) as excinfo: field.deserialize({1: "bar"}) assert excinfo.value.args[0] == { 1: {"key": ["Not a valid string."], "value": ["Not a valid number."]} } with pytest.raises(ValidationError) as excinfo: field.deserialize({"foo@test.com": "bar"}) assert excinfo.value.args[0] == { "foo@test.com": {"value": ["Not a valid number."]} } assert excinfo.value.valid_data == {} with pytest.raises(ValidationError) as excinfo: field.deserialize({1: 1}) assert excinfo.value.args[0] == {1: {"key": ["Not a valid string."]}} assert excinfo.value.valid_data == {} with pytest.raises(ValidationError) as excinfo: field.deserialize({"foo": "bar"}) assert excinfo.value.args[0] == { "foo": { "key": [ "Not a valid email address.", "String does not match expected pattern.", ], "value": ["Not a valid number."], } } assert excinfo.value.valid_data == {} def test_url_field_deserialization(self): field = fields.Url() assert field.deserialize("https://duckduckgo.com") == "https://duckduckgo.com" with pytest.raises(ValidationError) as excinfo: field.deserialize("badurl") assert excinfo.value.args[0][0] == "Not a valid URL." # Relative URLS not allowed by default with pytest.raises(ValidationError) as excinfo: field.deserialize("/foo/bar") assert excinfo.value.args[0][0] == "Not a valid URL." # regression test for https://github.com/marshmallow-code/marshmallow/issues/1400 def test_url_field_non_list_validators(self): field = fields.Url(validate=(validate.Length(min=16),)) with pytest.raises(ValidationError, match="Shorter than minimum length"): field.deserialize("https://abc.def") def test_relative_url_field_deserialization(self): field = fields.Url(relative=True) assert field.deserialize("/foo/bar") == "/foo/bar" def test_url_field_schemes_argument(self): field = fields.URL() url = "ws://test.test" with pytest.raises(ValidationError): field.deserialize(url) field2 = fields.URL(schemes={"http", "https", "ws"}) assert field2.deserialize(url) == url def test_email_field_deserialization(self): field = fields.Email() assert field.deserialize("foo@bar.com") == "foo@bar.com" with pytest.raises(ValidationError) as excinfo: field.deserialize("invalidemail") assert excinfo.value.args[0][0] == "Not a valid email address." field = fields.Email(validate=[validate.Length(min=12)]) with pytest.raises(ValidationError) as excinfo: field.deserialize("foo@bar.com") assert excinfo.value.args[0][0] == "Shorter than minimum length 12." # regression test for https://github.com/marshmallow-code/marshmallow/issues/1400 def test_email_field_non_list_validators(self): field = fields.Email(validate=(validate.Length(min=9),)) with pytest.raises(ValidationError, match="Shorter than minimum length"): field.deserialize("a@bc.com") def test_function_field_deserialization_is_noop_by_default(self): field = fields.Function(lambda x: None) # Default is noop assert field.deserialize("foo") == "foo" assert field.deserialize(42) == 42 def test_function_field_deserialization_with_callable(self): field = fields.Function(lambda x: None, deserialize=lambda val: val.upper()) assert field.deserialize("foo") == "FOO" def test_function_field_deserialization_with_context(self): class Parent(Schema): pass field = fields.Function( lambda x: None, deserialize=lambda val, context: val.upper() + context["key"], ) with pytest.warns(RemovedInMarshmallow4Warning): field.parent = Parent(context={"key": "BAR"}) assert field.deserialize("foo") == "FOOBAR" def test_function_field_passed_deserialize_only_is_load_only(self): field = fields.Function(deserialize=lambda val: val.upper()) assert field.load_only is True def test_function_field_passed_deserialize_and_serialize_is_not_load_only(self): field = fields.Function( serialize=lambda val: val.lower(), deserialize=lambda val: val.upper() ) assert field.load_only is False def test_uuid_field_deserialization(self): field = fields.UUID() uuid_str = str(uuid.uuid4()) result = field.deserialize(uuid_str) assert isinstance(result, uuid.UUID) assert str(result) == uuid_str uuid4 = uuid.uuid4() result = field.deserialize(uuid4) assert isinstance(result, uuid.UUID) assert result == uuid4 uuid_bytes = b"]\xc7wW\x132O\xf9\xa5\xbe\x13\x1f\x02\x18\xda\xbf" result = field.deserialize(uuid_bytes) assert isinstance(result, uuid.UUID) assert result.bytes == uuid_bytes @pytest.mark.parametrize("in_value", ["malformed", 123, [], b"tooshort"]) def test_invalid_uuid_deserialization(self, in_value): field = fields.UUID() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) assert excinfo.value.args[0] == "Not a valid UUID." def test_ip_field_deserialization(self): field = fields.IP() ipv4_str = "140.82.118.3" result = field.deserialize(ipv4_str) assert isinstance(result, ipaddress.IPv4Address) assert str(result) == ipv4_str ipv6_str = "2a00:1450:4001:824::200e" result = field.deserialize(ipv6_str) assert isinstance(result, ipaddress.IPv6Address) assert str(result) == ipv6_str @pytest.mark.parametrize( "in_value", ["malformed", 123, b"\x01\x02\03", "192.168", "192.168.0.1/24", "ff::aa:1::2"], ) def test_invalid_ip_deserialization(self, in_value): field = fields.IP() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) assert excinfo.value.args[0] == "Not a valid IP address." def test_ipv4_field_deserialization(self): field = fields.IPv4() ipv4_str = "140.82.118.3" result = field.deserialize(ipv4_str) assert isinstance(result, ipaddress.IPv4Address) assert str(result) == ipv4_str @pytest.mark.parametrize( "in_value", [ "malformed", 123, b"\x01\x02\03", "192.168", "192.168.0.1/24", "2a00:1450:4001:81d::200e", ], ) def test_invalid_ipv4_deserialization(self, in_value): field = fields.IPv4() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) assert excinfo.value.args[0] == "Not a valid IPv4 address." def test_ipv6_field_deserialization(self): field = fields.IPv6() ipv6_str = "2a00:1450:4001:824::200e" result = field.deserialize(ipv6_str) assert isinstance(result, ipaddress.IPv6Address) assert str(result) == ipv6_str def test_ipinterface_field_deserialization(self): field = fields.IPInterface() ipv4interface_str = "140.82.118.3/24" result = field.deserialize(ipv4interface_str) assert isinstance(result, ipaddress.IPv4Interface) assert str(result) == ipv4interface_str ipv6interface_str = "2a00:1450:4001:824::200e/128" result = field.deserialize(ipv6interface_str) assert isinstance(result, ipaddress.IPv6Interface) assert str(result) == ipv6interface_str @pytest.mark.parametrize( "in_value", [ "malformed", 123, b"\x01\x02\03", "192.168", "192.168.0.1/33", "ff::aa:1::2", "2a00:1450:4001:824::200e/129", ], ) def test_invalid_ipinterface_deserialization(self, in_value): field = fields.IPInterface() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) assert excinfo.value.args[0] == "Not a valid IP interface." def test_ipv4interface_field_deserialization(self): field = fields.IPv4Interface() ipv4interface_str = "140.82.118.3/24" result = field.deserialize(ipv4interface_str) assert isinstance(result, ipaddress.IPv4Interface) assert str(result) == ipv4interface_str @pytest.mark.parametrize( "in_value", [ "malformed", 123, b"\x01\x02\03", "192.168", "192.168.0.1/33", "2a00:1450:4001:81d::200e", "2a00:1450:4001:824::200e/129", ], ) def test_invalid_ipv4interface_deserialization(self, in_value): field = fields.IPv4Interface() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) assert excinfo.value.args[0] == "Not a valid IPv4 interface." def test_ipv6interface_field_deserialization(self): field = fields.IPv6Interface() ipv6interface_str = "2a00:1450:4001:824::200e/128" result = field.deserialize(ipv6interface_str) assert isinstance(result, ipaddress.IPv6Interface) assert str(result) == ipv6interface_str @pytest.mark.parametrize( "in_value", [ "malformed", 123, b"\x01\x02\03", "ff::aa:1::2", "192.168.0.1", "192.168.0.1/24", "2a00:1450:4001:824::200e/129", ], ) def test_invalid_ipv6interface_deserialization(self, in_value): field = fields.IPv6Interface() with pytest.raises(ValidationError) as excinfo: field.deserialize(in_value) assert excinfo.value.args[0] == "Not a valid IPv6 interface." def test_enum_field_by_symbol_deserialization(self): field = fields.Enum(GenderEnum) assert field.deserialize("male") == GenderEnum.male def test_enum_field_by_symbol_invalid_value(self): field = fields.Enum(GenderEnum) with pytest.raises( ValidationError, match="Must be one of: male, female, non_binary." ): field.deserialize("dummy") def test_enum_field_by_symbol_not_string(self): field = fields.Enum(GenderEnum) with pytest.raises(ValidationError, match="Not a valid string."): field.deserialize(12) def test_enum_field_by_value_true_deserialization(self): field = fields.Enum(HairColorEnum, by_value=True) assert field.deserialize("black hair") == HairColorEnum.black field = fields.Enum(GenderEnum, by_value=True) assert field.deserialize(1) == GenderEnum.male def test_enum_field_by_value_field_deserialization(self): field = fields.Enum(HairColorEnum, by_value=fields.String) assert field.deserialize("black hair") == HairColorEnum.black field = fields.Enum(GenderEnum, by_value=fields.Integer) assert field.deserialize(1) == GenderEnum.male field = fields.Enum(DateEnum, by_value=fields.Date(format="%d/%m/%Y")) assert field.deserialize("29/02/2004") == DateEnum.date_1 def test_enum_field_by_value_true_invalid_value(self): field = fields.Enum(HairColorEnum, by_value=True) with pytest.raises( ValidationError, match="Must be one of: black hair, brown hair, blond hair, red hair.", ): field.deserialize("dummy") field = fields.Enum(GenderEnum, by_value=True) with pytest.raises(ValidationError, match="Must be one of: 1, 2, 3."): field.deserialize(12) def test_enum_field_by_value_field_invalid_value(self): field = fields.Enum(HairColorEnum, by_value=fields.String) with pytest.raises( ValidationError, match="Must be one of: black hair, brown hair, blond hair, red hair.", ): field.deserialize("dummy") field = fields.Enum(GenderEnum, by_value=fields.Integer) with pytest.raises(ValidationError, match="Must be one of: 1, 2, 3."): field.deserialize(12) field = fields.Enum(DateEnum, by_value=fields.Date(format="%d/%m/%Y")) with pytest.raises( ValidationError, match="Must be one of: 29/02/2004, 29/02/2008, 29/02/2012." ): field.deserialize("28/02/2004") def test_enum_field_by_value_true_wrong_type(self): field = fields.Enum(HairColorEnum, by_value=True) with pytest.raises( ValidationError, match="Must be one of: black hair, brown hair, blond hair, red hair.", ): field.deserialize("dummy") field = fields.Enum(GenderEnum, by_value=True) with pytest.raises(ValidationError, match="Must be one of: 1, 2, 3."): field.deserialize(12) def test_enum_field_by_value_field_wrong_type(self): field = fields.Enum(HairColorEnum, by_value=fields.String) with pytest.raises(ValidationError, match="Not a valid string."): field.deserialize(12) field = fields.Enum(GenderEnum, by_value=fields.Integer) with pytest.raises(ValidationError, match="Not a valid integer."): field.deserialize("dummy") field = fields.Enum(DateEnum, by_value=fields.Date(format="%d/%m/%Y")) with pytest.raises(ValidationError, match="Not a valid date."): field.deserialize("30/02/2004") def test_deserialization_function_must_be_callable(self): with pytest.raises(TypeError): fields.Function(lambda x: None, deserialize="notvalid") def test_method_field_deserialization_is_noop_by_default(self): class MiniUserSchema(Schema): uppername = fields.Method("uppercase_name") def uppercase_name(self, obj): return obj.upper() s = MiniUserSchema() assert s.fields["uppername"].deserialize("steve") == "steve" def test_deserialization_method(self): class MiniUserSchema(Schema): uppername = fields.Method("uppercase_name", deserialize="lowercase_name") def uppercase_name(self, obj): return obj.name.upper() def lowercase_name(self, value): return value.lower() s = MiniUserSchema() assert s.fields["uppername"].deserialize("STEVE") == "steve" def test_deserialization_method_must_be_a_method(self): class BadSchema(Schema): uppername = fields.Method("uppercase_name", deserialize="lowercase_name") with pytest.raises(AttributeError): BadSchema() def test_method_field_deserialize_only(self): class MethodDeserializeOnly(Schema): name = fields.Method(deserialize="lowercase_name") def lowercase_name(self, value): return value.lower() assert MethodDeserializeOnly().load({"name": "ALEC"})["name"] == "alec" def test_datetime_list_field_deserialization(self): dtimes = dt.datetime.now(), dt.datetime.now(), dt.datetime.now(dt.timezone.utc) dstrings = [each.isoformat() for each in dtimes] field = fields.List(fields.DateTime()) result = field.deserialize(dstrings) assert all(isinstance(each, dt.datetime) for each in result) for actual, expected in zip(result, dtimes): assert_date_equal(actual, expected) def test_list_field_deserialize_invalid_item(self): field = fields.List(fields.DateTime) with pytest.raises(ValidationError) as excinfo: field.deserialize(["badvalue"]) assert excinfo.value.args[0] == {0: ["Not a valid datetime."]} field = fields.List(fields.Str()) with pytest.raises(ValidationError) as excinfo: field.deserialize(["good", 42]) assert excinfo.value.args[0] == {1: ["Not a valid string."]} def test_list_field_deserialize_multiple_invalid_items(self): field = fields.List( fields.Int( validate=validate.Range(10, 20, error="Value {input} not in range") ) ) with pytest.raises(ValidationError) as excinfo: field.deserialize([10, 5, 25]) assert len(excinfo.value.args[0]) == 2 assert excinfo.value.args[0][1] == ["Value 5 not in range"] assert excinfo.value.args[0][2] == ["Value 25 not in range"] @pytest.mark.parametrize("value", ["notalist", 42, {}]) def test_list_field_deserialize_value_that_is_not_a_list(self, value): field = fields.List(fields.Str()) with pytest.raises(ValidationError) as excinfo: field.deserialize(value) assert excinfo.value.args[0] == "Not a valid list." def test_datetime_int_tuple_field_deserialization(self): dtime = dt.datetime.now() data = dtime.isoformat(), 42 field = fields.Tuple([fields.DateTime(), fields.Integer()]) result = field.deserialize(data) assert isinstance(result, tuple) assert len(result) == 2 for val, type_, true_val in zip(result, (dt.datetime, int), (dtime, 42)): assert isinstance(val, type_) assert val == true_val def test_tuple_field_deserialize_invalid_item(self): field = fields.Tuple([fields.DateTime]) with pytest.raises(ValidationError) as excinfo: field.deserialize(["badvalue"]) assert excinfo.value.args[0] == {0: ["Not a valid datetime."]} field = fields.Tuple([fields.Str(), fields.Integer()]) with pytest.raises(ValidationError) as excinfo: field.deserialize(["good", "bad"]) assert excinfo.value.args[0] == {1: ["Not a valid integer."]} def test_tuple_field_deserialize_multiple_invalid_items(self): validator = validate.Range(10, 20, error="Value {input} not in range") field = fields.Tuple( [ fields.Int(validate=validator), fields.Int(validate=validator), fields.Int(validate=validator), ] ) with pytest.raises(ValidationError) as excinfo: field.deserialize([10, 5, 25]) assert len(excinfo.value.args[0]) == 2 assert excinfo.value.args[0][1] == ["Value 5 not in range"] assert excinfo.value.args[0][2] == ["Value 25 not in range"] @pytest.mark.parametrize("value", ["notalist", 42, {}]) def test_tuple_field_deserialize_value_that_is_not_a_collection(self, value): field = fields.Tuple([fields.Str()]) with pytest.raises(ValidationError) as excinfo: field.deserialize(value) assert excinfo.value.args[0] == "Not a valid tuple." def test_tuple_field_deserialize_invalid_length(self): field = fields.Tuple([fields.Str(), fields.Str()]) with pytest.raises(ValidationError) as excinfo: field.deserialize([42]) assert excinfo.value.args[0] == "Length must be 2." def test_constant_field_deserialization(self): field = fields.Constant("something") assert field.deserialize("whatever") == "something" def test_constant_is_always_included_in_deserialized_data(self): class MySchema(Schema): foo = fields.Constant(42) sch = MySchema() assert sch.load({})["foo"] == 42 assert sch.load({"foo": 24})["foo"] == 42 def test_field_deserialization_with_user_validator_function(self): field = fields.String(validate=lambda s: s.lower() == "valid") assert field.deserialize("Valid") == "Valid" with pytest.raises(ValidationError) as excinfo: field.deserialize("invalid") assert excinfo.value.args[0][0] == "Invalid value." assert type(excinfo.value) is ValidationError def test_field_deserialization_with_user_validator_class_that_returns_bool(self): class MyValidator: def __call__(self, val): if val == "valid": return True return False field = fields.Raw(validate=MyValidator()) assert field.deserialize("valid") == "valid" with pytest.raises(ValidationError, match="Invalid value."): field.deserialize("invalid") def test_field_deserialization_with_user_validator_that_raises_error_with_list( self, ): def validator(val): raise ValidationError(["err1", "err2"]) class MySchema(Schema): foo = fields.Raw(validate=validator) errors = MySchema().validate({"foo": 42}) assert errors["foo"] == ["err1", "err2"] def test_validator_must_return_false_to_raise_error(self): # validator returns None, so anything validates field = fields.String(validate=lambda s: None) assert field.deserialize("Valid") == "Valid" # validator returns False, so nothing validates field2 = fields.String(validate=lambda s: False) with pytest.raises(ValidationError): field2.deserialize("invalid") def test_field_deserialization_with_validator_with_nonascii_input(self): field = fields.String(validate=lambda s: False) with pytest.raises(ValidationError) as excinfo: field.deserialize("привет") assert type(excinfo.value) is ValidationError def test_field_deserialization_with_user_validators(self): validators_gen = ( func for func in ( lambda s: s.lower() == "valid", lambda s: s.lower()[::-1] == "dilav", ) ) m_colletion_type = [ fields.String( validate=[ lambda s: s.lower() == "valid", lambda s: s.lower()[::-1] == "dilav", ] ), fields.String( validate=( lambda s: s.lower() == "valid", lambda s: s.lower()[::-1] == "dilav", ) ), fields.String(validate=validators_gen), ] for field in m_colletion_type: assert field.deserialize("Valid") == "Valid" with pytest.raises(ValidationError, match="Invalid value."): field.deserialize("invalid") def test_field_deserialization_with_custom_error_message(self): field = fields.String( validate=lambda s: s.lower() == "valid", error_messages={"validator_failed": "Bad value."}, ) with pytest.raises(ValidationError, match="Bad value."): field.deserialize("invalid") # No custom deserialization behavior, so a dict is returned class SimpleUserSchema(Schema): name = fields.String() age = fields.Float() class Validator(Schema): email = fields.Email() colors = fields.Str(validate=validate.OneOf(["red", "blue"])) age = fields.Integer(validate=lambda n: n > 0) class Validators(Schema): email = fields.Email() colors = fields.Str(validate=validate.OneOf(["red", "blue"])) age = fields.Integer(validate=[lambda n: n > 0, lambda n: n < 100]) class TestSchemaDeserialization: def test_deserialize_to_dict(self): user_dict = {"name": "Monty", "age": "42.3"} result = SimpleUserSchema().load(user_dict) assert result["name"] == "Monty" assert math.isclose(result["age"], 42.3) def test_deserialize_with_missing_values(self): user_dict = {"name": "Monty"} result = SimpleUserSchema().load(user_dict) # 'age' is not included in result assert result == {"name": "Monty"} def test_deserialize_many(self): users_data = [{"name": "Mick", "age": "914"}, {"name": "Keith", "age": "8442"}] result = SimpleUserSchema(many=True).load(users_data) assert isinstance(result, list) user = result[0] assert user["age"] == int(users_data[0]["age"]) def test_exclude(self): schema = SimpleUserSchema(exclude=("age",), unknown=EXCLUDE) result = schema.load({"name": "Monty", "age": 42}) assert "name" in result assert "age" not in result def test_nested_single_deserialization_to_dict(self): class SimpleBlogSerializer(Schema): title = fields.String() author = fields.Nested(SimpleUserSchema, unknown=EXCLUDE) blog_dict = { "title": "Gimme Shelter", "author": {"name": "Mick", "age": "914", "email": "mick@stones.com"}, } result = SimpleBlogSerializer().load(blog_dict) author = result["author"] assert author["name"] == "Mick" assert author["age"] == 914 assert "email" not in author def test_nested_list_deserialization_to_dict(self): class SimpleBlogSerializer(Schema): title = fields.String() authors = fields.Nested(SimpleUserSchema, many=True) blog_dict = { "title": "Gimme Shelter", "authors": [ {"name": "Mick", "age": "914"}, {"name": "Keith", "age": "8442"}, ], } result = SimpleBlogSerializer().load(blog_dict) assert isinstance(result["authors"], list) author = result["authors"][0] assert author["name"] == "Mick" assert author["age"] == 914 def test_nested_single_none_not_allowed(self): class PetSchema(Schema): name = fields.Str() class OwnerSchema(Schema): pet = fields.Nested(PetSchema(), allow_none=False) sch = OwnerSchema() errors = sch.validate({"pet": None}) assert "pet" in errors assert errors["pet"] == ["Field may not be null."] def test_nested_many_non_not_allowed(self): class PetSchema(Schema): name = fields.Str() class StoreSchema(Schema): pets = fields.Nested(PetSchema, allow_none=False, many=True) sch = StoreSchema() errors = sch.validate({"pets": None}) assert "pets" in errors assert errors["pets"] == ["Field may not be null."] def test_nested_single_required_missing(self): class PetSchema(Schema): name = fields.Str() class OwnerSchema(Schema): pet = fields.Nested(PetSchema(), required=True) sch = OwnerSchema() errors = sch.validate({}) assert "pet" in errors assert errors["pet"] == ["Missing data for required field."] def test_nested_many_required_missing(self): class PetSchema(Schema): name = fields.Str() class StoreSchema(Schema): pets = fields.Nested(PetSchema, required=True, many=True) sch = StoreSchema() errors = sch.validate({}) assert "pets" in errors assert errors["pets"] == ["Missing data for required field."] def test_nested_only_basestring(self): class ANestedSchema(Schema): pk = fields.Str() class MainSchema(Schema): pk = fields.Str() child = fields.Pluck(ANestedSchema, "pk") sch = MainSchema() result = sch.load({"pk": "123", "child": "456"}) assert result["child"]["pk"] == "456" def test_nested_only_basestring_with_list_data(self): class ANestedSchema(Schema): pk = fields.Str() class MainSchema(Schema): pk = fields.Str() children = fields.Pluck(ANestedSchema, "pk", many=True) sch = MainSchema() result = sch.load({"pk": "123", "children": ["456", "789"]}) assert result["children"][0]["pk"] == "456" assert result["children"][1]["pk"] == "789" def test_nested_none_deserialization(self): class SimpleBlogSerializer(Schema): title = fields.String() author = fields.Nested(SimpleUserSchema, allow_none=True) blog_dict = {"title": "Gimme Shelter", "author": None} result = SimpleBlogSerializer().load(blog_dict) assert result["author"] is None assert result["title"] == blog_dict["title"] def test_deserialize_with_attribute_param(self): class AliasingUserSerializer(Schema): username = fields.Email(attribute="email") years = fields.Integer(attribute="age") data = {"username": "foo@bar.com", "years": "42"} result = AliasingUserSerializer().load(data) assert result["email"] == "foo@bar.com" assert result["age"] == 42 # regression test for https://github.com/marshmallow-code/marshmallow/issues/450 def test_deserialize_with_attribute_param_symmetry(self): class MySchema(Schema): foo = fields.Raw(attribute="bar.baz") schema = MySchema() dump_data = schema.dump({"bar": {"baz": 42}}) assert dump_data == {"foo": 42} load_data = schema.load({"foo": 42}) assert load_data == {"bar": {"baz": 42}} def test_deserialize_with_attribute_param_error_returns_field_name_not_attribute_name( self, ): class AliasingUserSerializer(Schema): username = fields.Email(attribute="email") years = fields.Integer(attribute="age") data = {"username": "foobar.com", "years": "42"} with pytest.raises(ValidationError) as excinfo: AliasingUserSerializer().load(data) errors = excinfo.value.messages assert errors["username"] == ["Not a valid email address."] def test_deserialize_with_attribute_param_error_returns_data_key_not_attribute_name( self, ): class AliasingUserSerializer(Schema): name = fields.String(data_key="Name") username = fields.Email(attribute="email", data_key="UserName") years = fields.Integer(attribute="age", data_key="Years") data = {"Name": "Mick", "UserName": "foobar.com", "Years": "abc"} with pytest.raises(ValidationError) as excinfo: AliasingUserSerializer().load(data) errors = excinfo.value.messages assert errors["UserName"] == ["Not a valid email address."] assert errors["Years"] == ["Not a valid integer."] def test_deserialize_with_data_key_param(self): class AliasingUserSerializer(Schema): name = fields.String(data_key="Name") username = fields.Email(attribute="email", data_key="UserName") years = fields.Integer(data_key="Years") data = {"Name": "Mick", "UserName": "foo@bar.com", "years": "42"} result = AliasingUserSerializer(unknown=EXCLUDE).load(data) assert result["name"] == "Mick" assert result["email"] == "foo@bar.com" assert "years" not in result def test_deserialize_with_data_key_as_empty_string(self): class MySchema(Schema): name = fields.Raw(data_key="") schema = MySchema() assert schema.load({"": "Grace"}) == {"name": "Grace"} def test_deserialize_with_dump_only_param(self): class AliasingUserSerializer(Schema): name = fields.String() years = fields.Integer(dump_only=True) size = fields.Integer(dump_only=True, load_only=True) nicknames = fields.List(fields.Str(), dump_only=True) data = { "name": "Mick", "years": "42", "size": "12", "nicknames": ["Your Majesty", "Brenda"], } result = AliasingUserSerializer(unknown=EXCLUDE).load(data) assert result["name"] == "Mick" assert "years" not in result assert "size" not in result assert "nicknames" not in result def test_deserialize_with_missing_param_value(self): bdate = dt.datetime(2017, 9, 29) class AliasingUserSerializer(Schema): name = fields.String() birthdate = fields.DateTime(load_default=bdate) data = {"name": "Mick"} result = AliasingUserSerializer().load(data) assert result["name"] == "Mick" assert result["birthdate"] == bdate def test_deserialize_with_missing_param_callable(self): bdate = dt.datetime(2017, 9, 29) class AliasingUserSerializer(Schema): name = fields.String() birthdate = fields.DateTime(load_default=lambda: bdate) data = {"name": "Mick"} result = AliasingUserSerializer().load(data) assert result["name"] == "Mick" assert result["birthdate"] == bdate def test_deserialize_with_missing_param_none(self): class AliasingUserSerializer(Schema): name = fields.String() years = fields.Integer(load_default=None, allow_none=True) data = {"name": "Mick"} result = AliasingUserSerializer().load(data) assert result["name"] == "Mick" assert result["years"] is None def test_deserialization_raises_with_errors(self): bad_data = {"email": "invalid-email", "colors": "burger", "age": -1} v = Validator() with pytest.raises(ValidationError) as excinfo: v.load(bad_data) errors = excinfo.value.messages assert "email" in errors assert "colors" in errors assert "age" in errors def test_deserialization_raises_with_errors_with_multiple_validators(self): bad_data = {"email": "invalid-email", "colors": "burger", "age": -1} v = Validators() with pytest.raises(ValidationError) as excinfo: v.load(bad_data) errors = excinfo.value.messages assert "email" in errors assert "colors" in errors assert "age" in errors def test_deserialization_many_raises_errors(self): bad_data = [ {"email": "foo@bar.com", "colors": "red", "age": 18}, {"email": "bad", "colors": "pizza", "age": -1}, ] v = Validator(many=True) with pytest.raises(ValidationError): v.load(bad_data) def test_validation_errors_are_stored(self): def validate_field(val): raise ValidationError("Something went wrong") class MySchema(Schema): foo = fields.Raw(validate=validate_field) with pytest.raises(ValidationError) as excinfo: MySchema().load({"foo": 42}) errors = excinfo.value.messages assert "Something went wrong" in errors["foo"] def test_multiple_errors_can_be_stored_for_a_field(self): def validate_with_bool(n): return False def validate_with_error(n): raise ValidationError("foo is not valid") class MySchema(Schema): foo = fields.Raw( required=True, validate=[validate_with_bool, validate_with_error] ) with pytest.raises(ValidationError) as excinfo: MySchema().load({"foo": "bar"}) errors = excinfo.value.messages assert type(errors["foo"]) is list assert len(errors["foo"]) == 2 def test_multiple_errors_can_be_stored_for_an_email_field(self): def validate_with_bool(val): return False class MySchema(Schema): email = fields.Email(validate=[validate_with_bool]) with pytest.raises(ValidationError) as excinfo: MySchema().load({"email": "foo"}) errors = excinfo.value.messages assert len(errors["email"]) == 2 assert "Not a valid email address." in errors["email"][0] def test_multiple_errors_can_be_stored_for_a_url_field(self): def validate_with_bool(val): return False class MySchema(Schema): url = fields.Url(validate=[validate_with_bool]) with pytest.raises(ValidationError) as excinfo: MySchema().load({"url": "foo"}) errors = excinfo.value.messages assert len(errors["url"]) == 2 assert "Not a valid URL." in errors["url"][0] def test_required_value_only_passed_to_validators_if_provided(self): class MySchema(Schema): foo = fields.Raw(required=True, validate=lambda f: False) with pytest.raises(ValidationError) as excinfo: MySchema().load({}) errors = excinfo.value.messages # required value missing assert len(errors["foo"]) == 1 assert "Missing data for required field." in errors["foo"] @pytest.mark.parametrize("partial_schema", [True, False]) def test_partial_deserialization(self, partial_schema): class MySchema(Schema): foo = fields.Raw(required=True) bar = fields.Raw(required=True) schema_args = {} load_args = {} if partial_schema: schema_args["partial"] = True else: load_args["partial"] = True data = MySchema(**schema_args).load({"foo": 3}, **load_args) assert data["foo"] == 3 assert "bar" not in data def test_partial_fields_deserialization(self): class MySchema(Schema): foo = fields.Raw(required=True) bar = fields.Raw(required=True) baz = fields.Raw(required=True) with pytest.raises(ValidationError) as excinfo: MySchema().load({"foo": 3}, partial=tuple()) data, errors = excinfo.value.valid_data, excinfo.value.messages assert data["foo"] == 3 assert "bar" in errors assert "baz" in errors data = MySchema().load({"foo": 3}, partial=("bar", "baz")) assert data["foo"] == 3 assert "bar" not in data assert "baz" not in data data = MySchema(partial=True).load({"foo": 3}, partial=("bar", "baz")) assert data["foo"] == 3 assert "bar" not in data assert "baz" not in data def test_partial_fields_validation(self): class MySchema(Schema): foo = fields.Raw(required=True) bar = fields.Raw(required=True) baz = fields.Raw(required=True) errors = MySchema().validate({"foo": 3}, partial=tuple()) assert "bar" in errors assert "baz" in errors errors = MySchema().validate({"foo": 3}, partial=("bar", "baz")) assert errors == {} errors = MySchema(partial=True).validate({"foo": 3}, partial=("bar", "baz")) assert errors == {} def test_unknown_fields_deserialization(self): class MySchema(Schema): foo = fields.Integer() data = MySchema(unknown=EXCLUDE).load({"foo": 3, "bar": 5}) assert data["foo"] == 3 assert "bar" not in data data = MySchema(unknown=INCLUDE).load({"foo": 3, "bar": 5}, unknown=EXCLUDE) assert data["foo"] == 3 assert "bar" not in data data = MySchema(unknown=EXCLUDE).load({"foo": 3, "bar": 5}, unknown=INCLUDE) assert data["foo"] == 3 assert data["bar"] data = MySchema(unknown=INCLUDE).load({"foo": 3, "bar": 5}) assert data["foo"] == 3 assert data["bar"] with pytest.raises(ValidationError, match="foo"): MySchema(unknown=INCLUDE).load({"foo": "asd", "bar": 5}) data = MySchema(unknown=INCLUDE, many=True).load( [{"foo": 1}, {"foo": 3, "bar": 5}] ) assert "foo" in data[1] assert "bar" in data[1] with pytest.raises(ValidationError) as excinfo: MySchema().load({"foo": 3, "bar": 5}) err = excinfo.value assert "bar" in err.messages assert err.messages["bar"] == ["Unknown field."] with pytest.raises(ValidationError) as excinfo: MySchema(many=True).load([{"foo": "abc"}, {"foo": 3, "bar": 5}]) err = excinfo.value assert 0 in err.messages assert "foo" in err.messages[0] assert err.messages[0]["foo"] == ["Not a valid integer."] assert 1 in err.messages assert "bar" in err.messages[1] assert err.messages[1]["bar"] == ["Unknown field."] def test_unknown_fields_deserialization_precedence(self): class MySchema(Schema): class Meta: unknown = INCLUDE foo = fields.Integer() data = MySchema().load({"foo": 3, "bar": 5}) assert data["foo"] == 3 assert data["bar"] == 5 data = MySchema(unknown=EXCLUDE).load({"foo": 3, "bar": 5}) assert data["foo"] == 3 assert "bar" not in data data = MySchema().load({"foo": 3, "bar": 5}, unknown=EXCLUDE) assert data["foo"] == 3 assert "bar" not in data with pytest.raises(ValidationError): MySchema(unknown=EXCLUDE).load({"foo": 3, "bar": 5}, unknown=RAISE) def test_unknown_fields_deserialization_with_data_key(self): class MySchema(Schema): foo = fields.Integer(data_key="Foo") data = MySchema().load({"Foo": 1}) assert data["foo"] == 1 assert "Foo" not in data data = MySchema(unknown=RAISE).load({"Foo": 1}) assert data["foo"] == 1 assert "Foo" not in data with pytest.raises(ValidationError): MySchema(unknown=RAISE).load({"foo": 1}) data = MySchema(unknown=INCLUDE).load({"Foo": 1}) assert data["foo"] == 1 assert "Foo" not in data def test_unknown_fields_deserialization_with_index_errors_false(self): class MySchema(Schema): foo = fields.Integer() class Meta: unknown = RAISE index_errors = False with pytest.raises(ValidationError) as excinfo: MySchema(many=True).load([{"foo": "invalid"}, {"foo": 42, "bar": 24}]) err = excinfo.value assert 1 not in err.messages assert "foo" in err.messages assert "bar" in err.messages assert err.messages["foo"] == ["Not a valid integer."] assert err.messages["bar"] == ["Unknown field."] def test_dump_only_fields_considered_unknown(self): class MySchema(Schema): foo = fields.Int(dump_only=True) with pytest.raises(ValidationError) as excinfo: MySchema().load({"foo": 42}) err = excinfo.value assert "foo" in err.messages assert err.messages["foo"] == ["Unknown field."] # When unknown = INCLUDE, dump-only fields are included as unknown # without any validation. data = MySchema(unknown=INCLUDE).load({"foo": "LOL"}) assert data["foo"] == "LOL" def test_unknown_fields_do_not_unpack_dotted_names(self): class MySchema(Schema): class Meta: unknown = INCLUDE foo = fields.Str() bar = fields.Str(data_key="bar.baz") # dotted names are still supported data = MySchema().load({"foo": "hi", "bar.baz": "okay"}) assert data == {"foo": "hi", "bar": "okay"} # but extra keys included via unknown=INCLUDE are not transformed into nested dicts data = MySchema().load({"foo": "hi", "bar.baz": "okay", "alpha.beta": "woah!"}) assert data == {"foo": "hi", "bar": "okay", "alpha.beta": "woah!"} validators_gen = (func for func in [lambda x: x <= 24, lambda x: 18 <= x]) validators_gen_float = (func for func in [lambda f: f <= 4.1, lambda f: f >= 1.0]) validators_gen_str = ( func for func in [lambda n: len(n) == 3, lambda n: n[1].lower() == "o"] ) class TestValidation: def test_integer_with_validator(self): field = fields.Integer(validate=lambda x: 18 <= x <= 24) out = field.deserialize("20") assert out == 20 with pytest.raises(ValidationError): field.deserialize(25) @pytest.mark.parametrize( "field", [ fields.Integer(validate=[lambda x: x <= 24, lambda x: 18 <= x]), fields.Integer(validate=(lambda x: x <= 24, lambda x: 18 <= x)), fields.Integer(validate=validators_gen), ], ) def test_integer_with_validators(self, field): out = field.deserialize("20") assert out == 20 with pytest.raises(ValidationError): field.deserialize(25) @pytest.mark.parametrize( "field", [ fields.Float(validate=[lambda f: f <= 4.1, lambda f: f >= 1.0]), fields.Float(validate=(lambda f: f <= 4.1, lambda f: f >= 1.0)), fields.Float(validate=validators_gen_float), ], ) def test_float_with_validators(self, field): assert field.deserialize(3.14) with pytest.raises(ValidationError): field.deserialize(4.2) def test_string_validator(self): field = fields.String(validate=lambda n: len(n) == 3) assert field.deserialize("Joe") == "Joe" with pytest.raises(ValidationError): field.deserialize("joseph") def test_function_validator(self): field = fields.Function( lambda d: d.name.upper(), validate=lambda n: len(n) == 3 ) assert field.deserialize("joe") with pytest.raises(ValidationError): field.deserialize("joseph") @pytest.mark.parametrize( "field", [ fields.Function( lambda d: d.name.upper(), validate=[lambda n: len(n) == 3, lambda n: n[1].lower() == "o"], ), fields.Function( lambda d: d.name.upper(), validate=(lambda n: len(n) == 3, lambda n: n[1].lower() == "o"), ), fields.Function(lambda d: d.name.upper(), validate=validators_gen_str), ], ) def test_function_validators(self, field): assert field.deserialize("joe") with pytest.raises(ValidationError): field.deserialize("joseph") def test_method_validator(self): class MethodSerializer(Schema): name = fields.Method( "get_name", deserialize="get_name", validate=lambda n: len(n) == 3 ) def get_name(self, val): return val.upper() assert MethodSerializer().load({"name": "joe"}) with pytest.raises(ValidationError, match="Invalid value."): MethodSerializer().load({"name": "joseph"}) # Regression test for https://github.com/marshmallow-code/marshmallow/issues/269 def test_nested_data_is_stored_when_validation_fails(self): class SchemaA(Schema): x = fields.Integer() y = fields.Integer(validate=lambda n: n > 0) z = fields.Integer() class SchemaB(Schema): w = fields.Integer() n = fields.Nested(SchemaA) sch = SchemaB() with pytest.raises(ValidationError) as excinfo: sch.load({"w": 90, "n": {"x": 90, "y": 89, "z": None}}) data, errors = excinfo.value.valid_data, excinfo.value.messages assert "z" in errors["n"] assert data == {"w": 90, "n": {"x": 90, "y": 89}} with pytest.raises(ValidationError) as excinfo: sch.load({"w": 90, "n": {"x": 90, "y": -1, "z": 180}}) data, errors = excinfo.value.valid_data, excinfo.value.messages assert "y" in errors["n"] assert data == {"w": 90, "n": {"x": 90, "z": 180}} def test_false_value_validation(self): class Sch(Schema): lamb = fields.Raw(validate=lambda x: x is False) equal = fields.Raw(validate=Equal(False)) errors = Sch().validate({"lamb": False, "equal": False}) assert not errors errors = Sch().validate({"lamb": True, "equal": True}) assert "lamb" in errors assert errors["lamb"] == ["Invalid value."] assert "equal" in errors assert errors["equal"] == ["Must be equal to False."] def test_nested_partial_load(self): class SchemaA(Schema): x = fields.Integer(required=True) y = fields.Integer() class SchemaB(Schema): z = fields.Nested(SchemaA) b_dict = {"z": {"y": 42}} # Partial loading shouldn't generate any errors. result = SchemaB().load(b_dict, partial=True) assert result["z"]["y"] == 42 # Non partial loading should complain about missing values. with pytest.raises(ValidationError) as excinfo: SchemaB().load(b_dict) data, errors = excinfo.value.valid_data, excinfo.value.messages assert data["z"]["y"] == 42 assert "z" in errors assert "x" in errors["z"] def test_deeply_nested_partial_load(self): class SchemaC(Schema): x = fields.Integer(required=True) y = fields.Integer() class SchemaB(Schema): c = fields.Nested(SchemaC) class SchemaA(Schema): b = fields.Nested(SchemaB) a_dict = {"b": {"c": {"y": 42}}} # Partial loading shouldn't generate any errors. result = SchemaA().load(a_dict, partial=True) assert result["b"]["c"]["y"] == 42 # Non partial loading should complain about missing values. with pytest.raises(ValidationError) as excinfo: SchemaA().load(a_dict) data, errors = excinfo.value.valid_data, excinfo.value.messages assert data["b"]["c"]["y"] == 42 assert "b" in errors assert "c" in errors["b"] assert "x" in errors["b"]["c"] def test_nested_partial_tuple(self): class SchemaA(Schema): x = fields.Integer(required=True) y = fields.Integer(required=True) class SchemaB(Schema): z = fields.Nested(SchemaA) b_dict = {"z": {"y": 42}} # If we ignore the missing z.x, z.y should still load. result = SchemaB().load(b_dict, partial=("z.x",)) assert result["z"]["y"] == 42 # If we ignore a missing z.y we should get a validation error. with pytest.raises(ValidationError): SchemaB().load(b_dict, partial=("z.y",)) def test_nested_partial_default(self): class SchemaA(Schema): x = fields.Integer(required=True) y = fields.Integer(required=True) class SchemaB(Schema): z = fields.Nested(SchemaA(partial=("x",))) b_dict = {"z": {"y": 42}} # Nested partial args should be respected. result = SchemaB().load(b_dict) assert result["z"]["y"] == 42 with pytest.raises(ValidationError): SchemaB().load({"z": {"x": 0}}) @pytest.mark.parametrize("FieldClass", ALL_FIELDS) def test_required_field_failure(FieldClass): # noqa class RequireSchema(Schema): age = FieldClass(required=True) user_data = {"name": "Phil"} with pytest.raises(ValidationError) as excinfo: RequireSchema().load(user_data) errors = excinfo.value.messages assert "Missing data for required field." in errors["age"] @pytest.mark.parametrize( "message", [ "My custom required message", {"error": "something", "code": 400}, ["first error", "second error"], ], ) def test_required_message_can_be_changed(message): class RequireSchema(Schema): age = fields.Integer(required=True, error_messages={"required": message}) user_data = {"name": "Phil"} with pytest.raises(ValidationError) as excinfo: RequireSchema().load(user_data) errors = excinfo.value.messages expected = [message] if isinstance(message, str) else message assert expected == errors["age"] @pytest.mark.parametrize("unknown", (EXCLUDE, INCLUDE, RAISE)) @pytest.mark.parametrize("data", [True, False, 42, None, []]) def test_deserialize_raises_exception_if_input_type_is_incorrect(data, unknown): class MySchema(Schema): foo = fields.Raw() bar = fields.Raw() with pytest.raises(ValidationError, match="Invalid input type.") as excinfo: MySchema(unknown=unknown).load(data) exc = excinfo.value assert list(exc.messages.keys()) == ["_schema"]