Skip to content

Commit bc3234d

Browse files
authored
Merge pull request #281 from yukinarit/alias
feat: Implement alias
2 parents db792cf + 36cbc6e commit bc3234d

File tree

10 files changed

+118
-3
lines changed

10 files changed

+118
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Foo(i=10, s='foo', f=100.0, b=True)
7373
- [Forward reference](docs/features/forward-reference.md)
7474
- [Case Conversion](docs/features/case-conversion.md)
7575
- [Rename](docs/features/rename.md)
76+
- [Alias](docs/features/alias.md)
7677
- [Skip](docs/features/skip.md)
7778
- [Conditional Skip](docs/features/conditional-skip.md)
7879
- [Custom field (de)serializer](docs/features/custom-field-serializer.md)

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Forward reference](features/forward-reference.md)
1616
- [Case Conversion](features/case-conversion.md)
1717
- [Rename](features/rename.md)
18+
- [Alias](features/alias.md)
1819
- [Skip](features/skip.md)
1920
- [Conditional Skip](features/conditional-skip.md)
2021
- [Custom field (de)serializer](features/custom-field-serializer.md)

docs/features/alias.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Alias
2+
3+
You can set aliases for field names. Alias only works for deserialization.
4+
5+
```python
6+
@serde
7+
@dataclass
8+
class Foo:
9+
a: str = field(alias=["b", "c"])
10+
```
11+
12+
`Foo` can be deserialized from either `{"a": "..."}`, `{"b": "..."}` or `{"c": "..."}`.
13+
14+
For complete example, please see [examples/alias.py](https://github.com/yukinarit/pyserde/blob/master/examples/alias.py)

docs/features/rename.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Rename
22

3-
In case you want to use a keyword as field such as `class`, you can use `serde_rename` field attribute.
3+
In case you want to use a keyword as field such as `class`, you can use `rename` field attribute. If you want to have multiple aliases, you can use [alias](alias.md).
44

55
```python
66
@serde
77
@dataclass
88
class Foo:
9-
class_name: str = field(metadata={'serde_rename': 'class'})
9+
class_name: str = field(rename='class')
1010

1111
print(to_json(Foo(class_name='Foo')))
1212
```

docs/introduction.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Foo(i=10, s='foo', f=100.0, b=True)
5757
- [Forward reference](features/forward-reference.md)
5858
- [Case Conversion](features/case-conversion.md)
5959
- [Rename](features/rename.md)
60+
- [Alias](features/alias.md)
6061
- [Skip](features/skip.md)
6162
- [Conditional Skip](features/conditional-skip.md)
6263
- [Custom field (de)serializer](features/custom-field-serializer.md)

examples/alias.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from dataclasses import dataclass
2+
3+
from serde import field, serde
4+
from serde.json import from_json
5+
6+
7+
@serde
8+
@dataclass
9+
class Foo:
10+
a: int = field(alias=["b", "c", "d"])
11+
12+
13+
def main():
14+
s = '{"a": 10}'
15+
print(f"From Json: {from_json(Foo, s)}")
16+
17+
s = '{"b": 20}'
18+
print(f"From Json: {from_json(Foo, s)}")
19+
20+
s = '{"c": 30}'
21+
print(f"From Json: {from_json(Foo, s)}")
22+
23+
s = '{"d": 40}'
24+
print(f"From Json: {from_json(Foo, s)}")
25+
26+
try:
27+
s = '{"e": 50}'
28+
print(f"From Json: {from_json(Foo, s)}")
29+
except Exception:
30+
pass
31+
32+
33+
if __name__ == '__main__':
34+
main()

examples/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22

3+
import alias
34
import any
45
import class_var
56
import collection
@@ -67,6 +68,7 @@ def run_all():
6768
run(ellipsis)
6869
run(init_var)
6970
run(class_var)
71+
run(alias)
7072
if PY310:
7173
import union_operator
7274

serde/core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ class FlattenOpts:
321321
def field(
322322
*args,
323323
rename: Optional[str] = None,
324+
alias: Optional[List[str]] = None,
324325
skip: Optional[bool] = None,
325326
skip_if: Optional[Callable] = None,
326327
skip_if_false: Optional[bool] = None,
@@ -339,6 +340,8 @@ def field(
339340

340341
if rename is not None:
341342
metadata["serde_rename"] = rename
343+
if alias is not None:
344+
metadata["serde_alias"] = alias
342345
if skip is not None:
343346
metadata["serde_skip"] = skip
344347
if skip_if is not None:
@@ -432,6 +435,7 @@ class Foo:
432435
compare: Any = field(default_factory=dataclasses._MISSING_TYPE)
433436
metadata: Mapping[str, Any] = field(default_factory=dict)
434437
case: Optional[str] = None
438+
alias: List[str] = field(default_factory=list)
435439
rename: Optional[str] = None
436440
skip: Optional[bool] = None
437441
skip_if: Optional[Func] = None
@@ -486,6 +490,7 @@ def from_dataclass(cls, f: dataclasses.Field) -> 'Field':
486490
compare=f.compare,
487491
metadata=f.metadata,
488492
rename=f.metadata.get('serde_rename'),
493+
alias=f.metadata.get('serde_alias', []),
489494
skip=f.metadata.get('serde_skip'),
490495
skip_if=skip_if or skip_if_false_func or skip_if_default_func,
491496
serializer=serializer,

serde/de.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
is_numpy_scalar,
8080
)
8181

82-
__all__: List = ['deserialize', 'is_deserializable', 'from_dict', 'from_tuple']
82+
__all__ = ['deserialize', 'is_deserializable', 'from_dict', 'from_tuple']
8383

8484
# Interface of Custom deserialize function.
8585
DeserializeFunc = Callable[[Type, Any], Any]
@@ -108,6 +108,15 @@ def default_deserializer(_cls: Type, obj):
108108
"""
109109

110110

111+
def _get_by_aliases(d: Dict[str, str], aliases: List[str]):
112+
if not aliases:
113+
raise KeyError("Tried all aliases, but key not found")
114+
if aliases[0] in d:
115+
return d[aliases[0]]
116+
else:
117+
return _get_by_aliases(d, aliases[1:])
118+
119+
111120
def _make_deserialize(
112121
cls_name: str,
113122
fields,
@@ -233,6 +242,7 @@ def wrap(cls: Type):
233242
g['TypeCheck'] = TypeCheck
234243
g['NoCheck'] = NoCheck
235244
g['coerce'] = coerce
245+
g['_get_by_aliases'] = _get_by_aliases
236246
if deserialize:
237247
g['serde_custom_class_deserializer'] = functools.partial(
238248
serde_custom_class_deserializer, custom=deserializer
@@ -755,6 +765,9 @@ def primitive(self, arg: DeField, suppress_coerce: bool = False) -> str:
755765
"""
756766
typ = typename(arg.type)
757767
dat = arg.data
768+
if arg.alias:
769+
aliases = map(lambda s: f'"{s}"', [arg.name, *arg.alias])
770+
dat = f"_get_by_aliases(data, [{','.join(aliases)}])"
758771
if self.suppress_coerce:
759772
return dat
760773
else:

tests/test_basics.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,50 @@ class Foo:
454454
assert f == de(Foo, se(f))
455455

456456

457+
@pytest.mark.parametrize('se,de', (format_dict + format_json + format_yaml + format_toml))
458+
def test_alias(se, de):
459+
@serde.serde
460+
class Foo:
461+
a: str = serde.field(alias=["b", "c", "d"])
462+
463+
f = Foo(a='foo')
464+
assert f == de(Foo, se(f))
465+
466+
467+
def test_conflicting_alias():
468+
@serde.serde
469+
class Foo:
470+
a: int = serde.field(alias=["b", "c", "d"])
471+
b: int
472+
c: int
473+
d: int
474+
475+
f = Foo(a=1, b=2, c=3, d=4)
476+
assert '{"a":1,"b":2,"c":3,"d":4}' == serde.json.to_json(f)
477+
ff = serde.json.from_json(Foo, '{"a":1,"b":2,"c":3,"d":4}')
478+
assert ff.a == 1
479+
assert ff.b == 2
480+
assert ff.c == 3
481+
assert ff.d == 4
482+
483+
ff = serde.json.from_json(Foo, '{"b":2,"c":3,"d":4}')
484+
assert ff.a == 2
485+
assert ff.b == 2
486+
assert ff.c == 3
487+
assert ff.d == 4
488+
489+
490+
def test_rename_and_alias():
491+
@serde.serde
492+
class Foo:
493+
a: int = serde.field(rename="z", alias=["b", "c", "d"])
494+
495+
f = Foo(a=1)
496+
assert '{"z":1}' == serde.json.to_json(f)
497+
ff = serde.json.from_json(Foo, '{"b":10}')
498+
assert ff.a == 10
499+
500+
457501
@pytest.mark.parametrize('se,de', (format_dict + format_json + format_msgpack + format_yaml + format_toml))
458502
def test_skip_if(se, de):
459503
@serde.serde

0 commit comments

Comments
 (0)