Functionality
One-to-one JSON property to object property mapping
Model:
class Person:
def __init__(self):
self.name = "Bob Smith"
JSON:
{
"full_name": "<person.name>"
}
To define that: The JSON "full_name" property is set from the object's "name" property. The object's "name" property is set from the JSON's "full_name" property.
mapping_schema = [
JsonPropertyMapping("full_name", "name")
]
Build classes that can serialize/deserialize Person
instances:
PersonJSONEncoder = MappingJSONEncoderClassBuilder(Person, mapping_schema).build()
PersonJSONDecoder = MappingJSONDecoderClassBuilder(Person, mapping_schema).build()
Serialize/deserialize instance of Person
using Python's inbuilt json.dumps
and json.loads
:
person_as_json = json.dumps(person, cls=PersonJSONEncoder)
person = json.loads("<person_as_json>", cls=PersonJSONDecoder)
Arbitrary mapping to JSON property value
Model:
class Person:
def __init__(self):
self.name = "Bob Smith"
def get_first_name(self) -> str:
return self.name.split(" ")[0]
def get_family_name(self) -> str:
return self.name.split(" ")[1]
JSON:
{
"first_name": "<person.get_first_name()>",
"family_name": "<person.get_family_name()>"
}
To define that:
Serialization to the JSON "first_name" property value uses the object's get_first_name
method.
Serialization to the JSON "family_name" property value uses the object's get_family_name
method.
mapping_schema = [
JsonPropertyMapping("first_name", object_property_getter=lambda person: person.get_first_name()),
JsonPropertyMapping("family_name", object_property_getter=lambda person: person.get_family_name())
]
See next section for how to do the reverse mapping.
Arbitrary mapping to object property value
Model:
class Person:
def __init__(self):
self.name = None
JSON:
{
"first_name": "<person.name.split(' ')[0]>",
"family_name": "<person.name.split(' ')[1]>"
}
To define that: * The object's name property value is derived from the value of both the "first_name" and "family name" JSON property values:
mapping_schema = [
JsonPropertyMapping("name", json_property_getter=lambda obj_as_dict: "%s %s" % (obj_as_dict["first_name"],
obj_as_dict["family_name"]))
]
Deserializing objects with constructors parameters
Model:
class Person:
def __init__(self, constructor_name: str):
self.name = name
JSON:
{
"full_name": "<person.name>"
}
To define that: Deserialization requires the value of the JSON "full_name" property be binded to the "constructor_name" parameter in the constructor. Serialization to the JSON "full_name" property value uses the object's "name" property value.
mapping_schema = [
JsonPropertyMapping("full_name", "name", object_constructor_parameter_name="constructor_name")
]
If further modification of the decoded value is needed, object_constructor_argument_modifier
can be set as a function
that takes the value retrieved by json_property_getter
after it is decoded by using the decoder_cls
JSON encoder and
returns the value that is binded to the constructor parameter.
Deserializing objects with mutators
Model:
class Person:
def __init__(self):
self._name = None
def set_name(name: str):
self._name = name
JSON:
{
"full_name": "<person._name>"
}
To define that: * Deserialization requires the private "_name" property be set via the "set_name" mutator from the "full_name" JSON property.
mapping_schema = [
JsonPropertyMapping("full_name", object_property_setter=lambda person, name: person.set_name(name))
]
Conditionally optional JSON properties
Model:
class Person:
def __init__(self):
self.name = name
JSON:
// if person.name is not None:
{
"full_name": "<person.name>"
}
// else:
{
// No `full_name` property
}
To define that:
* JSON representation should only include the "full_name" if it is not None
(e.g. to reduce the size of the JSON).
def add_name_to_json_if_not_none(json_as_dict: dict, name: Optional[str]):
if name is not None:
json_as_dict["full_name"] = name
mapping_schema = [JsonPropertyMapping("full_name", json_property_setter=add_name_to_json_if_not_none)]
Inheritance
Models:
class Identifiable:
def __init__(self):
self.id = None
class Named:
def __init__(self):
self.name = None
class Employee(Identifiable, Named):
def __init__(self):
super().__init__()
self.title = None
JSON:
{
"identifier": "<employee.id>",
"full_name": "<employee.name>",
"job_title": "<employee.title>"
}
To define that:
* Serialization of Employee
should "extend" the way Named
and Identifiable
are serialized.
identifiable_mapping_schema = [JsonPropertyMapping("identifier", "id")]
named_mapping_schema = [JsonPropertyMapping("full_name", "name")]
employee_mapping_schema = [JsonPropertyMapping("job_title", "title")]
IdentifiableJSONEncoder = MappingJSONEncoderClassBuilder(Identifiable, identifiable_mapping_schema).build()
NamedJSONEncoder = MappingJSONEncoderClassBuilder(Named, named_mapping_schema).build()
EmployeeJSONEncoder = MappingJSONEncoderClassBuilder(Employee, employee_mapping_schema, (IdentifiableJSONEncoder, NamedJSONEncoder)).build()
IdentifiableJSONDecoder = MappingJSONDecoderClassBuilder(Identifiable, identifiable_mapping_schema).build()
NamedJSONDecoder = MappingJSONDecoderClassBuilder(Named, named_mapping_schema).build()
EmployeeJSONDecoder = MappingJSONDecoderClassBuilder(Employee, employee_mapping_schema, (IdentifiableJSONDecoder, NamedJSONDecoder)).build()
Note: Each value mapping can be "overriden" by encoders used afterwards. Mappings of properties for superclasses are
done before those defined in the current class; the mappings for superclasses are completed in the order defined by the
tuple. e.g. In the example above, the mappings defined in IdentifiableJSONDecoder
are applied first, then those in
NamedJSONDecoder
, followed by those in EmployeeJSONDecoder
. If EmployeeJSONDecoder
redefined a mapping for the
id
object property, the value for this property would first be written by the mapper from IdentifiableJSONDecoder
before been overwritten by a mapper defined in EmployeeJSONDecoder
.
For obvious reasons, the mappings to constructor parameters defined in superclasses are not used.
Nested complex objects
Model:
class Person:
def __init__(self):
self.name = None
class Team:
def __init__(self):
self.moto = None
self.people = [] # type: List[Person]
JSON:
{
"team_moto": "<team.moto>",
"members": "<[person in team.people]>"
}
To define that:
* Person
instances, nested inside Employee
objects, should be serialized and deserialized by specific encoder and
decoders.
employee_mapping_schema = [
JsonPropertyMapping("team_moto", "moto"),
JsonPropertyMapping("members", "people", encoder_cls=PersonJSONEncoder, decoder_cls=PersonJSONDecoder)
]
If a property is encoded/decoded by the same encoder/decoder that is currently being defined, the type should be returned by a function to work around the scoping problem, e.g.:
class Person:
def __init__(self):
self.nemesis = None
PersonJSONEncoder = MappingJSONEncoderClassBuilder(Person, [
JsonPropertyMapping("nemesis", "nemesis", encoder_cls=lambda: PersonJSONEncoder)
]).build()
One-way mappings
Contrived example warning...
Model:
class Person:
def __init__(self):
self.name = None
self.age = None
JSON input:
{
"full_name": "<person.name>"
}
JSON output:
{
"age": "<person.age>"
}
To define that: Serialization should ignore the object's "name" property. Deserialization only with the JSON's "full_name" property.
mapping_schema = [
JsonPropertyMapping(
json_property_getter=lambda json_as_dict: json_as_dict["full_name"],
object_property_setter=lambda person, name: person.set_name(name)
),
JsonPropertyMapping(
json_property_setter=lambda json_as_dict, age: json_as_dict.__setitem__("age", age),
object_property_getter=lambda person: person.age
)
]
Casting JSON primitives
To help with casting JSON primitives, the following decoders/encoders are provided:
StrJSONEncoder
: serializes value to a string (e.g. object property=123
-> JSON property="123"
).StrJSONDecoder
: deserializes value as a string (e.g. JSON property=123
-> object property="123"
).IntJSONEncoder
: serializes value to an int (e.g. object property="123"
-> JSON property=123
).IntJSONDecoder
: deserializes value as an int (e.g. JSON property="123"
-> object property=123
).FloatJSONEncoder
: serializes value to a float (e.g. object property="123.5"
-> JSON property=123.5
).FloatJSONDecoder
: deserializes value as an float (e.g. JSON property="12.3"
-> object property=12.3
).DatetimeEpochJSONEncoder
: serializes datetime to epoch, truncated to seconds (e.g. object property=datetime(1970, 1, 1, tzinfo=timezone.utc)
-> JSON property=0
).DatetimeEpochJSONDecoder
: deserializes datetime as epoch (e.g. JSON property=0
-> object property=datetime(1970, 1, 1, tzinfo=timezone.utc)
).DatetimeISOFormatJSONEncoder
: serializes datetime to a ISO 8601 datetime representation (e.g. object property=datetime(1970, 1, 1, tzinfo=timezone.utc)
-> JSON property=1970-01-01T00:00:00+00:00
).DatetimeISOFormatJSONDecoder
: deserializes ISO 8601 datetime representation to a datetime (e.g. JSON property="1970-01-01T00:00:00+00:00"
-> object property=datetime(1970, 1, 1, tzinfo=timezone.utc)
).
Model:
class Person:
def __init__(self):
self.age = 42
JSON:
{
"years_old": "str(<person.age>)"
}
To define that:
* The age property of Person
instances should be an int
but given as a string in the JSON representation.
person_mapping_schema = [
JsonPropertyMapping("years_old", "age", encoder_cls=StrJSONEncoder, decoder_cls=IntJSONDecoder)
]
Optional parameters
Model:
class Person:
def __init__(self):
self.name = None
self.age = 42
JSON:
{
"years_old": "<person.age>"
}
To define that:
A JSON parameter is optional (i.e. it may/may not appear in the JSON representation).
An object parameter should not be included in the JSON if it takes the value None
.
person_mapping_schema = [
JsonPropertyMapping("full_name", "name", optional=True),
JsonPropertyMapping("years_old", "age")
]
Sets
JSON supports less "primitive types" than Python, implying that there cannot be an unambiguous, one-to-one mapping
between all Python and JSON "primitive types". One such type without an equivalent in JSON is set
. Python's built-in
JSON library "handles" the serialiation of sets by raising a TypeError
.
SetJSONEncoder
and SetJSONDecoder
are supplied in this library to support the serialization of set
s. They work by
encoding sets as JSON lists then decoding these lists back into sets. As the mapping between JSON and objects is well
defined when using this library (i.e. it never has to guess the type of the Python object that is to be constructed from
a JSON representation), it is known if a JSON list should be decoded as list
or as set
.
Build classes that can serialize/deserialize sets of strings:
StringSetJSONEncoder = SetJSONEncoderClassBuilder(StrJSONEncoder).build()
StringSetJSONDecoder = SetJSONDecoderClassBuilder(StrJSONDecoder).build()
Model:
class Person:
def __init__(self):
self.nicknames = {"Rob", "Bob"}
JSON:
{
"short_names": ["<person.nicknames>"]
}
To define that:
The JSON "short_names" property is set from the object's "nicknames" property, which is of type set
.
The object's "nicknames" property is set from the JSON's "full_name" property.
mapping_schema = [
JsonPropertyMapping("short_names", "nicknames", encoder_cls=StringSetJSONEncoder, decoder_cls=StringSetJSONDecoder)
]
Serialization to/from a dict
To serialize an object to a dictionary, opposed to a string:
custom_object_as_dict = CustomJSONEncoder().default(custom_object) # type: dict
You can use this with any encoder that inherits from JSONEncoder
and overrides default
.
To deserialize an object from a dictionary, opposed to from a string:
custom_object = CustomJSONDecoder().decode_parsed(custom_object_as_dict)
You can only use this with decoders defined by this library as they implement the
ParsedJSONDecoder
interface. To
achieve this functionality with other JSONDecoder
implementations, you would have to (wastefully) convert the
dictionary to a string using json.dump
before using the decoder's standard decode
method.