Skip to content

Commit 773d5d7

Browse files
committed
Add missing_values parameter to field
Allows specifying which values are treated as "missing". Addresses #713
1 parent 4290d0f commit 773d5d7

File tree

3 files changed

+64
-6
lines changed

3 files changed

+64
-6
lines changed

src/marshmallow/fields.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ class Field(FieldABC):
134134
"validator_failed": "Invalid value.",
135135
}
136136

137+
default_missing_values = tuple()
138+
137139
def __init__(
138140
self,
139141
*,
@@ -147,6 +149,7 @@ def __init__(
147149
load_only=False,
148150
dump_only=False,
149151
error_messages=None,
152+
missing_values=None,
150153
**metadata
151154
):
152155
self.default = default
@@ -168,9 +171,14 @@ def __init__(
168171
"or a collection of callables."
169172
)
170173

171-
# If missing=None, None should be considered valid by default
174+
self.missing_values = (
175+
missing_values
176+
if missing_values is not None
177+
else self.default_missing_values
178+
)
179+
# If missing=None or None is in missing_values, None should be considered valid by default
172180
if allow_none is None:
173-
if missing is None:
181+
if self._is_missing_value(None):
174182
self.allow_none = True
175183
else:
176184
self.allow_none = False
@@ -223,6 +231,9 @@ def get_value(self, obj, attr, accessor=None, default=missing_):
223231
check_key = attr if attribute is None else attribute
224232
return accessor_func(obj, check_key, default)
225233

234+
def _is_missing_value(self, value):
235+
return value is missing_ or value in self.missing_values
236+
226237
def _validate(self, value):
227238
"""Perform validation on ``value``. Raise a :exc:`ValidationError` if validation
228239
does not succeed.
@@ -279,7 +290,7 @@ def _validate_missing(self, value):
279290
"""Validate missing values. Raise a :exc:`ValidationError` if
280291
`value` should be considered missing.
281292
"""
282-
if value is missing_:
293+
if self._is_missing_value(value):
283294
if hasattr(self, "required") and self.required:
284295
raise self.make_error("required")
285296
if value is None:
@@ -319,7 +330,7 @@ def deserialize(self, value, attr=None, data=None, **kwargs):
319330
# Validate required fields, deserialize, then validate
320331
# deserialized value
321332
self._validate_missing(value)
322-
if value is missing_:
333+
if self._is_missing_value(value):
323334
_miss = self.missing
324335
return _miss() if callable(_miss) else _miss
325336
if getattr(self, "allow_none", False) is True and value is None:

tests/test_deserialization.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_fields_dont_allow_none_by_default(self, FieldClass):
2525
with pytest.raises(ValidationError, match="Field may not be null."):
2626
field.deserialize(None)
2727

28-
def test_allow_none_is_true_if_missing_is_true(self):
28+
def test_allow_none_is_true_if_missing_is_none(self):
2929
field = fields.Field(missing=None)
3030
assert field.allow_none is True
3131
field.deserialize(None) is None
@@ -1338,6 +1338,53 @@ class AliasingUserSerializer(Schema):
13381338
assert result["name"] == "Mick"
13391339
assert result["years"] is None
13401340

1341+
# https://github.com/marshmallow-code/marshmallow/issues/713
1342+
@pytest.mark.parametrize(
1343+
("missing", "missing_values", "input_data", "expected"),
1344+
[
1345+
(None, {None}, {"name": None}, {"name": None}),
1346+
(None, {None}, {"name": ""}, {"name": ""}),
1347+
(None, {""}, {"name": ""}, {"name": None}),
1348+
(None, {""}, {}, {"name": None}),
1349+
("", {""}, {"name": ""}, {"name": ""}),
1350+
("", {None}, {"name": None}, {"name": ""}),
1351+
("", {None}, {}, {"name": ""}),
1352+
],
1353+
)
1354+
def test_deserialize_with_custom_missing_values(
1355+
self, missing, missing_values, input_data, expected
1356+
):
1357+
class ArtistSchema(Schema):
1358+
name = fields.String(missing=missing, missing_values=missing_values)
1359+
1360+
schema = ArtistSchema()
1361+
assert schema.load(input_data) == expected
1362+
1363+
def test_deserialize_required_field_with_custom_missing_values(self):
1364+
class ArtistSchema(Schema):
1365+
album_names = fields.List(
1366+
fields.Str(), required=True, missing_values=([], ())
1367+
)
1368+
1369+
with pytest.raises(ValidationError, match="required"):
1370+
ArtistSchema().load({"album_names": []})
1371+
1372+
def test_setting_default_missing_values(self, monkeypatch):
1373+
monkeypatch.setattr(fields.Field, "default_missing_values", ("",))
1374+
monkeypatch.setattr(fields.List, "default_missing_values", ([], ()))
1375+
1376+
class ArtistSchema(Schema):
1377+
name = fields.String(missing=None)
1378+
dob = fields.DateTime(missing=None)
1379+
album_names = fields.List(fields.Str(), required=True)
1380+
1381+
schema = ArtistSchema()
1382+
loaded = schema.load({"name": "", "dob": "", "album_names": ["Hunky Dory"]})
1383+
assert loaded == {"name": None, "dob": None, "album_names": ["Hunky Dory"]}
1384+
1385+
with pytest.raises(ValidationError, match="required"):
1386+
assert schema.load({"name": "", "dob": "", "album_names": []})
1387+
13411388
def test_deserialization_raises_with_errors(self):
13421389
bad_data = {"email": "invalid-email", "colors": "burger", "age": -1}
13431390
v = Validator()

tests/test_serialization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def test_function_field_load_only(self):
8181
field = fields.Function(deserialize=lambda obj: None)
8282
assert field.load_only
8383

84-
def test_function_field_passed_serialize_with_context(self, user, monkeypatch):
84+
def test_function_field_passed_serialize_with_context(self, user):
8585
class Parent(Schema):
8686
pass
8787

0 commit comments

Comments
 (0)