Skip to content

Commit 7feec4c

Browse files
committed
Issue #145
1 parent a6bc6e9 commit 7feec4c

File tree

9 files changed

+112
-6
lines changed

9 files changed

+112
-6
lines changed

docs/serialization.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,3 +846,42 @@ To illustrate the usage, examine the following snippet:
846846
{"type": "sales", "name": "joe"},
847847
]
848848
}
849+
850+
851+
Force a Field To Be Set As A Constant During Deserialization
852+
============================================================
853+
(new in 2.13.0)
854+
Imagine a use-case in which you have an abstract base class "Car", and several cars that inherit from it. One of
855+
the fields you might want to have is "maker":
856+
857+
.. code-block:: python
858+
859+
class Car(AbstractStructure):
860+
maker: str
861+
...
862+
863+
class SubaruOutback(Car):
864+
...
865+
866+
class AcuraMVX(Car):
867+
...
868+
869+
870+
When you deserialize SubaruOutback or AcuraMVX, you should set the "maker" explicitly to a predefined value.
871+
Typedpy supports it, by defining "Constant" in the serialization mapper (similar to Version Mapping). See below:
872+
873+
.. code-block:: python
874+
875+
class SubaruOutback(Car):
876+
...
877+
878+
_deserialization_mapper = {"maker": Constant("Subaru")}
879+
880+
881+
class AcuraMVX(Car):
882+
...
883+
884+
_deserialization_mapper = {"maker": Constant("Acura")}
885+
886+
This means that the field "maker" will ignore the input to the deserializer, and always set to the value defined
887+
in the mapper.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typedpy import *
2+
3+
4+
class Example1(Structure):
5+
name = String()
6+
7+
_required = ['name']
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"definitions": {
3+
},
4+
"example": {
5+
"type": "object",
6+
"properties": {
7+
"name": {"type": "string"}
8+
},
9+
"required": ["name"],
10+
"additionalProperties": true
11+
}
12+
}
13+

tests/schema_mapping/structures/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
from .example8 import Example8
99
from .example9 import Example9, Example10
1010
from .example11 import Example11
11+
from .example12 import Example12
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typedpy import Constant, Structure, mappers
2+
3+
4+
class A(Structure):
5+
i: int
6+
7+
_serialization_mapper = mappers.TO_LOWERCASE
8+
9+
10+
class Example12(A):
11+
a: str
12+
_serialization_mapper = {"I": Constant(5), "A": "name"}

tests/schema_mapping/test_json_schema_code_mapping.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def wrapped(file_name: str) -> str:
5252
("Example9", "example9_schema.json", "generated_example9.py"),
5353
("Example10", "example10_schema.json", "generated_example10.py"),
5454
("Example11", "example11_schema.json", "generated_example11.py"),
55+
("Example12", "example12_schema.json", "generated_example12.py"),
5556
],
5657
)
5758
@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9 or higher")

tests/test_deserialization.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from typedpy import (
1010
Boolean,
11-
ImmutableStructure,
11+
Constant, ImmutableStructure,
1212
Structure,
1313
Array,
1414
Number,
@@ -1342,3 +1342,32 @@ class Foo(Structure):
13421342

13431343
foo = Deserializer(Foo).deserialize({"p": [1, 2, 3]})
13441344
assert foo.p.x == 1
1345+
1346+
1347+
def test_serialization_constant1():
1348+
class A(Structure):
1349+
i: int
1350+
1351+
_serialization_mapper = mappers.TO_LOWERCASE
1352+
1353+
class B(A):
1354+
a: str
1355+
_serialization_mapper = {"I": Constant(5), "A": "number"}
1356+
1357+
assert Deserializer(B).deserialize({"number": "xyz"}, keep_undefined=False) == B(a="xyz", i=5)
1358+
assert Serializer(B(a="xyz", i=5)).serialize() == {'i': 5, 'number': 'xyz'}
1359+
1360+
1361+
def test_serialization_constant_in_list():
1362+
class A(Structure):
1363+
i: int
1364+
1365+
_serialization_mapper = {"i": Constant(10)}
1366+
1367+
class B(Structure):
1368+
arr: Array[A]
1369+
_serialization_mapper = [mappers.TO_LOWERCASE, {"ARR": "numbers"}]
1370+
1371+
assert Deserializer(B).deserialize({"numbers": [{}, {}]}, keep_undefined=False) == B(arr=[A(10), A(10)])
1372+
assert Serializer(B(arr=[A(10), A(10)])).serialize() == {'numbers': [{'i': 10}, {'i': 10}]}
1373+

typedpy/json_schema_mapping.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .enum import Enum
3131

3232
from .extfields import DateString
33-
from .mappers import DoNotSerialize, aggregate_serialization_mappers
33+
from .mappers import Constant, DoNotSerialize, aggregate_serialization_mappers
3434
from .structures import ADDITIONAL_PROPERTIES, NoneField, Structure
3535

3636
SCHEMA_PATTERN_PROPERTIES = "patternProperties"
@@ -102,7 +102,7 @@ def _validated_mapped_value(mapper, key):
102102
"This is unsupported by code-to-schema conversion. "
103103
"You will need to manually fix it."
104104
)
105-
elif key_mapper is DoNotSerialize:
105+
elif key_mapper is DoNotSerialize or isinstance(key_mapper, Constant):
106106
return key_mapper
107107
elif not isinstance(key_mapper, (FunctionCall, str)):
108108
raise TypeError("mapper must have a FunctionCall or a string")
@@ -160,10 +160,10 @@ def structure_to_schema(structure, definitions_schema, serialization_mapper=None
160160
else key
161161
)
162162
mapped_value = _validated_mapped_value(mapper, key)
163-
if mapped_value is DoNotSerialize:
163+
if mapped_value is DoNotSerialize or isinstance(mapped_value, Constant):
164164
if mapped_key in required:
165165
required.pop(required.index(mapped_key))
166-
if mapped_value is not DoNotSerialize:
166+
else:
167167
if key in required:
168168
required[required.index(key)] = mapped_key
169169
sub_mapper = mapper.get(f"{key}._mapper", {})

typedpy/serialization.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .commons import deep_get, raise_errs_if_needed
88
from .versioned_mapping import VERSION_MAPPING, Versioned, convert_dict
99
from .mappers import (
10-
DoNotSerialize,
10+
Constant, DoNotSerialize,
1111
aggregate_deserialization_mappers,
1212
aggregate_serialization_mappers,
1313
mappers,
@@ -600,6 +600,8 @@ def _get_arg_list(key_mapper):
600600
elif isinstance(key_mapper, (str,)):
601601
val = deep_get(the_dict, key_mapper)
602602
processed_input = val if val is not None else the_dict.get(key)
603+
elif isinstance(key_mapper, Constant):
604+
processed_input = key_mapper()
603605
else:
604606
raise TypeError(
605607
f"mapper value must be a key in the input or a FunctionCal. Got {wrap_val(key_mapper)}"
@@ -756,6 +758,8 @@ def _get_mapped_value(mapper, key, items):
756758
return key_mapper.func(*args)
757759
elif key_mapper is DoNotSerialize:
758760
return key_mapper
761+
elif isinstance(key_mapper, Constant):
762+
return key_mapper()
759763
elif not isinstance(key_mapper, (FunctionCall, str)):
760764
raise TypeError("mapper must have a FunctionCall or a string")
761765

0 commit comments

Comments
 (0)