Extending from Meta
¶
There are a couple well-known use cases where we might want to customize
behavior of how fields are transformed during the JSON load and dump
process (for example, to camel case or snake case), or when we want
datetime
and date
objects to be converted to an epoch timestamp
(as an int
) instead of the default behavior, which converts these
objects to their ISO 8601 string representation via
isoformat.
Such common behaviors can be easily specified on a per-class basis by
defining an inner class which extends from JSONSerializable.Meta
(or the
aliased name JSONWizard.Meta
), as shown below. The name of the inner class
does not matter, but for demo purposes it’s named the same as the base class here.
Note
As of v0.18.0, the Meta config for the main dataclass will “cascade down”
and be merged with the Meta config (if specified) of each nested dataclass. To
disable this behavior, you can pass in recursive=False
to the Meta config.
import logging
from dataclasses import dataclass
from datetime import date
from dataclass_wizard import JSONSerializable
from dataclass_wizard.enums import DateTimeTo, LetterCase
# Sets up logging, so that library logs are visible in the console.
logging.basicConfig(level='INFO')
@dataclass
class MyClass(JSONSerializable):
class Meta(JSONSerializable.Meta):
# True to enable Debug mode for additional (more verbose) log output.
#
# For example, a message is logged whenever an unknown JSON key is
# encountered when `from_dict` or `from_json` is called.
#
# This also results in more helpful messages during error handling, which
# can be useful when debugging the cause when values are an invalid type
# (i.e. they don't match the annotation for the field) when unmarshalling
# a JSON object to a dataclass instance.
#
# Note there is a minor performance impact when DEBUG mode is enabled.
debug_enabled = True
# When enabled, a specified Meta config for the main dataclass (i.e. the
# class on which `from_dict` and `to_dict` is called) will cascade down
# and be merged with the Meta config for each *nested* dataclass; note
# that during a merge, priority is given to the Meta config specified on
# each class.
#
# The default behavior is True, so the Meta config (if provided) will
# apply in a recursive manner.
recursive = True
# True to raise an class:`UnknownJSONKey` when an unmapped JSON key is
# encountered when `from_dict` or `from_json` is called; an unknown key is
# one that does not have a known mapping to a dataclass field.
#
# The default is to only log a "warning" for such cases, which is visible
# when `debug_enabled` is true and logging is properly configured.
raise_on_unknown_json_key = False
# A customized mapping of JSON keys to dataclass fields, that is used
# whenever `from_dict` or `from_json` is called.
#
# Note: this is in addition to the implicit field transformations, like
# "myStr" -> "my_str"
#
# If the reverse mapping is also desired (i.e. dataclass field to JSON
# key), then specify the "__all__" key as a truthy value. If multiple JSON
# keys are specified for a dataclass field, only the first one provided is
# used in this case.
json_key_to_field = {}
# How should :class:`date` and :class:`datetime` objects be serialized
# when converted to a Python dictionary object or a JSON string.
marshal_date_time_as = DateTimeTo.TIMESTAMP
# How JSON keys should be transformed to dataclass fields.
key_transform_with_load = LetterCase.PASCAL
# How dataclass fields should be transformed to JSON keys.
key_transform_with_dump = LetterCase.SNAKE
# The field name that identifies the tag for a class.
#
# When set to a value, a '__tag__' field will be populated in the
# dictionary object in the dump (serialization) process. When loading
# (or de-serializing) a dictionary object, the '__tag__' field will be
# used to load the corresponding dataclass, assuming the dataclass field
# is properly annotated as a Union type, ex.:
# my_data: Union[Data1, Data2, Data3]
tag = ''
# The dictionary key that identifies the tag field for a class. This is
# only set when the `tag` field or the `auto_assign_tags` flag is enabled
# in the `Meta` config for a dataclass.
#
# Defaults to '__tag__' if not specified.
tag_key = ''
# Auto-assign the class name as a dictionary "tag" key, for any dataclass
# fields which are in a `Union` declaration, ex.:
# my_data: Union[Data1, Data2, Data3]
auto_assign_tags = False
# Determines whether we should we skip / omit fields with default values
# (based on the `default` or `default_factory` argument specified for
# the :func:`dataclasses.field`) in the serialization process.
skip_defaults = True
MyStr: str
MyDate: date
data = {'my_str': 'test', 'myDATE': '2010-12-30'}
c = MyClass.from_dict(data)
print(repr(c))
# prints:
# MyClass(MyStr='test', MyDate=datetime.date(2010, 12, 30))
string = c.to_json()
print(string)
# prints:
# {"my_str": "test", "my_date": 1293685200}
Note that the key_transform_...
attributes only apply to the field
names that are defined in the dataclass; other keys such as the ones for
TypedDict
or NamedTuple
sub-classes won’t be similarly
transformed. If you need similar behavior for any of the typing
sub-classes mentioned, simply convert them to dataclasses and the key
transform should then apply for those fields.
Any Meta
settings only affect a class model¶
All attributes set in the Meta
class will only apply to the
class model that from_dict
or to_dict
runs on; that is,
it will apply recursively to any nested dataclasses by default, and
merge with the Meta
config (if specified) for each class. Note that
you can pass recursive=False
in the Meta
config, if you only want
it to apply to the main dataclass, and not to any nested dataclasses
in the model.
When the Meta
config for the main dataclass is merged with any nested
dataclass, priority is given to any fields explicitly set in the Meta
config for each class. In addition, the following attributes in each class’s
Meta
are excluded from a merge:
recursive
json_key_to_field
tag
Also, note that a Meta
config should not affect the load/dump process
for other, unrelated dataclasses. Though if you do desire this behavior, see
the Global Meta Settings section below.
Here’s a quick example to confirm this behavior:
import logging
from dataclasses import dataclass
from datetime import date
from dataclass_wizard import JSONWizard
# Sets up logging, so that library logs are visible in the console.
logging.basicConfig(level='INFO')
@dataclass
class FirstClass(JSONWizard):
class _(JSONWizard.Meta):
debug_enabled = True
marshal_date_time_as = 'Timestamp'
key_transform_with_load = 'PASCAL'
key_transform_with_dump = 'SNAKE'
MyStr: str
MyNestedClass: 'MyNestedClass'
@dataclass
class MyNestedClass:
MyDate: date
@dataclass
class SecondClass(JSONWizard):
# If `SecondClass` were to define it's own `Meta` class, those changes
# would only be applied to `SecondClass` and any nested dataclass
# by default.
# class _(JSONWizard.Meta):
# key_transform_with_dump = 'PASCAL'
my_str: str
my_date: date
def main():
data = {'my_str': 'test', 'myNestedClass': {'myDATE': '2010-12-30'}}
c1 = FirstClass.from_dict(data)
print(repr(c1))
# prints:
# FirstClass(MyStr='test', MyNestedClass=MyNestedClass(MyDate=datetime.date(2010, 12, 30)))
string = c1.to_json()
print(string)
# prints:
# {"my_str": "test", "my_nested_class": {"my_date": 1293685200}}
data2 = {'my_str': 'test', 'myDATE': '2022-01-15'}
c2 = SecondClass.from_dict(data2)
print(repr(c2))
# prints:
# SecondClass(my_str='test', my_date=datetime.date(2022, 1, 15))
string = c2.to_json()
print(string)
# prints:
# {"myStr": "test", "myDate": "2022-01-15"}
if __name__ == '__main__':
main()
Global Meta
settings¶
In case you want global Meta
settings that will apply to
all dataclasses which sub-class from JSONWizard
, you
can simply define JSONWizard.Meta
as an outer class
as shown in the example below.
Attention
Although not recommended, a global Meta
class should resolve the issue.
Note that this is a specialized use case and should be considered carefully.
This may also have unforeseen consequences - for example, if your application
depends on another library that uses the JSONWizard
Mixin class from the
Dataclass Wizard library, then that library will be likewise affected by any
global Meta
values that are set.
import logging
from dataclasses import dataclass
from datetime import date
from dataclass_wizard import JSONWizard
from dataclass_wizard.enums import DateTimeTo
# Sets up logging, so that library logs are visible in the console.
logging.basicConfig(level='INFO')
class GlobalJSONMeta(JSONWizard.Meta):
"""
Global settings for the JSON load/dump process, that should apply to
*all* subclasses of `JSONWizard`.
Note: it does not matter where this class is defined, as long as it's
declared before any methods in `JSONWizard` are called.
"""
debug_enabled = True
marshal_date_time_as = DateTimeTo.TIMESTAMP
key_transform_with_load = 'PASCAL'
key_transform_with_dump = 'SNAKE'
@dataclass
class FirstClass(JSONWizard):
MyStr: str
MyDate: date
@dataclass
class SecondClass(JSONWizard):
# If `SecondClass` were to define it's own `Meta` class, those changes
# will effectively override the global `Meta` settings below, but only
# for `SecondClass` itself and no other dataclass.
# class _(JSONWizard.Meta):
# key_transform_with_dump = 'CAMEL'
AnotherStr: str
OtherDate: date
def main():
data1 = {'my_str': 'test', 'myDATE': '2010-12-30'}
c1 = FirstClass.from_dict(data1)
print(repr(c1))
# prints:
# FirstClass(MyStr='test', MyDate=datetime.date(2010, 12, 30))
string = c1.to_json()
print(string)
# prints:
# {"my_str": "test", "my_date": 1293685200}
data2 = {'another_str': 'test', 'OtherDate': '2010-12-30'}
c2 = SecondClass.from_dict(data2)
print(repr(c2))
# prints:
# SecondClass(AnotherStr='test', OtherDate=datetime.date(2010, 12, 30))
string = c2.to_json()
print(string)
# prints:
# {"another_str": "test", "other_date": 1293685200}
if __name__ == '__main__':
main()