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:

  • debug_enabled

  • 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()