Skip to content

Commit 6aeb49e

Browse files
authored
Merge pull request #605 from yukinarit/deny-unknown-fields
Implement deny_unknown_fields class attribute
2 parents f93cafa + dac6c33 commit 6aeb49e

File tree

7 files changed

+154
-3
lines changed

7 files changed

+154
-3
lines changed

docs/en/class-attributes.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,23 @@ class Foo:
163163
See [examples/class_var.py](https://github.com/yukinarit/pyserde/blob/main/examples/class_var.py) for complete example.
164164

165165
[^1]: [dataclasses.fields](https://docs.python.org/3/library/dataclasses.html#dataclasses.fields)
166+
167+
### **`deny_unknown_fields`**
168+
169+
New in v0.22.0, the `deny_unknown_fields` option in the pyserde decorator allows you to enforce strict field validation during deserialization. When this option is enabled, any fields in the input data that are not defined in the target class will cause deserialization to fail with a `SerdeError`.
170+
171+
Consider the following example:
172+
```python
173+
@serde(deny_unknown_fields=True)
174+
class Foo:
175+
a: int
176+
b: str
177+
```
178+
179+
With `deny_unknown_fields=True`, attempting to deserialize data containing fields beyond those defined (a and b in this case) will raise an error. For instance:
180+
```
181+
from_json(Foo, '{"a": 10, "b": "foo", "c": 100.0, "d": true}')
182+
```
183+
This will raise a `SerdeError` since fields c and d are not recognized members of Foo.
184+
185+
See [examples/deny_unknown_fields.py](https://github.com/yukinarit/pyserde/blob/main/examples/deny_unknown_fields.py) for complete example.

docs/ja/class-attributes.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,26 @@ class Foo:
178178
a: ClassVar[int] = 10
179179
```
180180

181-
完全な例については、[examples/class_var.py](https://github.com/yukinarit/pyserde/blob/main/examples/class_var.py) を参照してください。
181+
完全な例については、[examples/class_var.py](https://github.com/yukinarit/pyserde/blob/main/examples/class_var.py)を参照してください。
182182

183183
[^1]: [dataclasses.fields](https://docs.python.org/3/library/dataclasses.html#dataclasses.fields)
184+
185+
### **`deny_unknown_fields`**
186+
187+
バージョン0.22.0で新規追加。 pyserdeデコレータの`deny_unknown_fields`オプションはデシリアライズ時のより厳格なフィールドチェックを制御できます。このオプションをTrueにするとデシリアライズ時に宣言されていないフィールドが見つかると`SerdeError`を投げることができます。
188+
189+
以下の例を考えてください。
190+
```python
191+
@serde(deny_unknown_fields=True)
192+
class Foo:
193+
a: int
194+
b: str
195+
```
196+
197+
`deny_unknown_fields=True`が指定されていると、 宣言されているフィールド(この場合aとb)以外がインプットにあると例外を投げます。例えば、
198+
```
199+
from_json(Foo, '{"a": 10, "b": "foo", "c": 100.0, "d": true}')
200+
```
201+
上記のコードはフィールドcとdという宣言されていないフィールドがあるためエラーとなります。
202+
203+
完全な例については、[examples/deny_unknown_fields.py](https://github.com/yukinarit/pyserde/blob/main/examples/deny_unknown_fields.py)を参照してください。

examples/deny_unknown_fields.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from serde import serde, SerdeError
2+
from serde.json import from_json
3+
4+
5+
@serde(deny_unknown_fields=True)
6+
class Foo:
7+
a: int
8+
b: str
9+
10+
11+
def main() -> None:
12+
try:
13+
s = '{"a": 10, "b": "foo", "c": 100.0, "d": true}'
14+
print(f"From Json: {from_json(Foo, s)}")
15+
except SerdeError:
16+
pass
17+
18+
19+
if __name__ == "__main__":
20+
main()

examples/runner.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def run_all() -> None:
3535
import pep681
3636
import plain_dataclass
3737
import plain_dataclass_class_attribute
38+
import deny_unknown_fields
3839
import python_pickle
3940
import recursive
4041
import recursive_list
@@ -107,6 +108,7 @@ def run_all() -> None:
107108
run(class_var)
108109
run(plain_dataclass)
109110
run(plain_dataclass_class_attribute)
111+
run(deny_unknown_fields)
110112
run(msg_pack)
111113
run(primitive_subclass)
112114
run(kw_only)
@@ -133,6 +135,6 @@ def run(module: typing.Any) -> None:
133135
try:
134136
run_all()
135137
print("-----------------")
136-
print("all tests passed successfully!")
138+
print("all examples completed successfully!")
137139
except Exception:
138140
sys.exit(1)

serde/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def serde(
124124
serialize_class_var: bool = False,
125125
class_serializer: Optional[ClassSerializer] = None,
126126
class_deserializer: Optional[ClassDeserializer] = None,
127+
deny_unknown_fields: bool = False,
127128
) -> Type[T]: ...
128129

129130

@@ -140,6 +141,7 @@ def serde(
140141
serialize_class_var: bool = False,
141142
class_serializer: Optional[ClassSerializer] = None,
142143
class_deserializer: Optional[ClassDeserializer] = None,
144+
deny_unknown_fields: bool = False,
143145
) -> Callable[[type[T]], type[T]]: ...
144146

145147

@@ -156,6 +158,7 @@ def serde(
156158
serialize_class_var: bool = False,
157159
class_serializer: Optional[ClassSerializer] = None,
158160
class_deserializer: Optional[ClassDeserializer] = None,
161+
deny_unknown_fields: bool = False,
159162
) -> Any:
160163
"""
161164
serde decorator. Keyword arguments are passed in `serialize` and `deserialize`.
@@ -187,6 +190,7 @@ def wrap(cls: Any) -> Any:
187190
type_check=type_check,
188191
serialize_class_var=serialize_class_var,
189192
class_deserializer=class_deserializer,
193+
deny_unknown_fields=deny_unknown_fields,
190194
)
191195
return cls
192196

serde/de.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ def deserialize(
192192
tagging: Tagging = DefaultTagging,
193193
type_check: TypeCheck = strict,
194194
class_deserializer: Optional[ClassDeserializer] = None,
195+
deny_unknown_fields: bool = False,
195196
**kwargs: Any,
196197
) -> type[T]:
197198
"""
@@ -329,7 +330,12 @@ def wrap(cls: type[T]) -> type[T]:
329330
scope,
330331
FROM_DICT,
331332
render_from_dict(
332-
cls, rename_all, deserializer, type_check, class_deserializer=class_deserializer
333+
cls,
334+
rename_all,
335+
deserializer,
336+
type_check,
337+
class_deserializer=class_deserializer,
338+
deny_unknown_fields=deny_unknown_fields,
333339
),
334340
g,
335341
)
@@ -1041,6 +1047,13 @@ def {{func}}(cls=cls, maybe_generic=None, maybe_generic_type_vars=None, data=Non
10411047
if reuse_instances is None:
10421048
reuse_instances = {{serde_scope.reuse_instances_default}}
10431049
1050+
{% if deny_unknown_fields %}
1051+
known_fields = {{ known_fields }}
1052+
unknown_fields = set((data or {}).keys()) - known_fields
1053+
if unknown_fields:
1054+
raise SerdeError(f'unknown fields: {unknown_fields}, expected one of {known_fields}')
1055+
{% endif %}
1056+
10441057
maybe_generic_type_vars = maybe_generic_type_vars or {{cls_type_vars}}
10451058
10461059
{% for f in fields %}
@@ -1143,12 +1156,18 @@ def render_from_iter(
11431156
return res
11441157

11451158

1159+
def get_known_fields(f: DeField[Any], rename_all: Optional[str]) -> list[str]:
1160+
names: list[str] = [f.conv_name(rename_all)]
1161+
return names + f.alias
1162+
1163+
11461164
def render_from_dict(
11471165
cls: type[Any],
11481166
rename_all: Optional[str] = None,
11491167
legacy_class_deserializer: Optional[DeserializeFunc] = None,
11501168
type_check: TypeCheck = strict,
11511169
class_deserializer: Optional[ClassDeserializer] = None,
1170+
deny_unknown_fields: bool = False,
11521171
) -> str:
11531172
renderer = Renderer(
11541173
FROM_DICT,
@@ -1159,6 +1178,9 @@ def render_from_dict(
11591178
class_name=typename(cls),
11601179
)
11611180
fields = list(filter(renderable, defields(cls)))
1181+
known_fields = set(
1182+
itertools.chain.from_iterable([get_known_fields(f, rename_all) for f in fields])
1183+
)
11621184
res = jinja2_env.get_template("dict").render(
11631185
func=FROM_DICT,
11641186
serde_scope=getattr(cls, SERDE_SCOPE),
@@ -1167,6 +1189,8 @@ def render_from_dict(
11671189
cls_type_vars=get_type_var_names(cls),
11681190
rvalue=renderer.render,
11691191
arg=functools.partial(to_arg, rename_all=rename_all),
1192+
deny_unknown_fields=deny_unknown_fields,
1193+
known_fields=known_fields,
11701194
)
11711195

11721196
if renderer.import_numpy:

tests/test_de.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import pytest
12
from decimal import Decimal
23
from typing import Union, Optional
4+
from serde import serde, SerdeError, field
5+
from serde.json import from_json
36
from serde.de import deserialize, from_obj, Renderer, DeField
47

58

@@ -125,3 +128,61 @@ class Foo:
125128
rendered_foo = f"Foo.__serde__.funcs['foo'](data=data[\"f\"], {kwargs})"
126129
rendered_opt = f'({rendered_foo}) if data.get("f") is not None else None'
127130
assert rendered == rendered_opt
131+
132+
133+
def test_deny_unknown_fields() -> None:
134+
@serde(deny_unknown_fields=True)
135+
class Foo:
136+
a: int
137+
b: str
138+
139+
with pytest.raises(SerdeError):
140+
from_json(Foo, '{"a": 10, "b": "foo", "c": 100.0, "d": true}')
141+
142+
f = from_json(Foo, '{"a": 10, "b": "foo"}')
143+
assert f.a == 10
144+
assert f.b == "foo"
145+
146+
147+
def test_deny_renamed_unknown_fields() -> None:
148+
@serde(deny_unknown_fields=True)
149+
class Foo:
150+
a: int
151+
b: str = field(rename="B")
152+
153+
with pytest.raises(SerdeError):
154+
from_json(Foo, '{"a": 10, "b": "foo"}')
155+
156+
f = from_json(Foo, '{"a": 10, "B": "foo"}')
157+
assert f.a == 10
158+
assert f.b == "foo"
159+
160+
@serde(rename_all="constcase", deny_unknown_fields=True)
161+
class Bar:
162+
a: int
163+
b: str
164+
165+
with pytest.raises(SerdeError):
166+
from_json(Bar, '{"a": 10, "b": "foo"}')
167+
168+
b = from_json(Bar, '{"A": 10, "B": "foo"}')
169+
assert b.a == 10
170+
assert b.b == "foo"
171+
172+
173+
def test_deny_aliased_unknown_fields() -> None:
174+
@serde(deny_unknown_fields=True)
175+
class Foo:
176+
a: int
177+
b: str = field(alias=["B"]) # type: ignore
178+
179+
with pytest.raises(SerdeError):
180+
from_json(Foo, '{"a": 10, "b": "foo", "c": 100.0, "d": true}')
181+
182+
f = from_json(Foo, '{"a": 10, "b": "foo"}')
183+
assert f.a == 10
184+
assert f.b == "foo"
185+
186+
f = from_json(Foo, '{"a": 10, "B": "foo"}')
187+
assert f.a == 10
188+
assert f.b == "foo"

0 commit comments

Comments
 (0)