Source code for dataclass_wizard.models

from collections.abc import Mapping
from dataclasses import MISSING
from dataclasses import Field as _Field
from typing import NewType

from ._type_def import ExplicitNull
from .constants import PY310_OR_ABOVE, PY314_OR_ABOVE
from .utils._object_path import split_object_path

# Define a simple type (alias) for the `CatchAll` field
#
# The `type` statement is introduced in Python 3.12
# Ref: https://docs.python.org/3.12/reference/simple_stmts.html#type
#
# TODO: uncomment following usage of `type` statement
#   once we drop support for Python 3.9 - 3.11
# if PY312_OR_ABOVE:
#     type CatchAll = Mapping
CatchAll = NewType('CatchAll', Mapping)


def _normalize_alias_path_args(all_paths, load, dump):
    """Normalize `AliasPath` arguments and canonicalize path values."""
    if load is not None:
        all_paths = load
        load = None
        dump = ExplicitNull

    elif dump is not None:
        all_paths = dump
        dump = None
        load = ExplicitNull

    if isinstance(all_paths, str):
        all_paths = (split_object_path(all_paths),)
    else:
        all_paths = tuple([
            split_object_path(a) if isinstance(a, str) else a
            for a in all_paths
        ])

    return all_paths, load, dump


def _normalize_alias_args(default, default_factory, all_aliases, load, dump, env):
    """Normalize `Alias` arguments and canonicalize alias values."""

    if default is not MISSING and default_factory is not MISSING:
        raise ValueError('cannot specify both default and default_factory')

    if all_aliases:
        load = dump = all_aliases

    elif load is not None and isinstance(load, str):
        load = (load,)

    elif env is not None:
        if isinstance(env, str):
            env = (env,)
        elif env is True:
            env = load

    return all_aliases, load, dump, env


# Instances of Field are only ever created from within this module,
# and only from the field() function, although Field instances are
# exposed externally as (conceptually) read-only objects.
#
# name and type are filled in after the fact, not in __init__.
# They're not known at the time this class is instantiated, but it's
# convenient if they're available later.

# noinspection PyPep8Naming,PyShadowingBuiltins
[docs] def Env(*load, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, **field_kwargs): # noinspection PyTypeChecker return Alias( env=load, default=default, default_factory=default_factory, init=init, repr=repr, hash=hash, compare=compare, metadata=metadata, **field_kwargs, )
# In Python 3.14, dataclasses adds a new parameter to the :class:`Field` # constructor: `doc` # # Ref: https://docs.python.org/3.14/library/dataclasses.html#dataclasses.field if PY314_OR_ABOVE: # noinspection PyPep8Naming,PyShadowingBuiltins def Alias( *all, load=None, dump=None, env=None, skip=False, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None, ): all, load, dump, env = _normalize_alias_args(default, default_factory, all, load, dump, env) return Field( load, dump, env, skip, None, default, default_factory, init, repr, hash, compare, metadata, kw_only, doc, ) # noinspection PyPep8Naming,PyShadowingBuiltins def AliasPath( *all, load=None, dump=None, skip=False, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None, ): all, load, dump = _normalize_alias_path_args(all, load, dump) return Field( load, dump, load, skip, all, default, default_factory, init, repr, hash, compare, metadata, kw_only, doc, ) # noinspection PyShadowingBuiltins def skip_if_field( condition, *, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None, ): if default is not MISSING and default_factory is not MISSING: raise ValueError("cannot specify both default and default_factory") if metadata is None: metadata = {} metadata["__skip_if__"] = condition return _Field( default, default_factory, init, repr, hash, compare, metadata, kw_only, doc, ) class Field(_Field): __slots__ = ("load_alias", "dump_alias", "env_vars", "skip", "path") # noinspection PyShadowingBuiltins def __init__( self, load_alias, dump_alias, env_vars, skip, path, default, default_factory, init, repr, hash, compare, metadata, kw_only, doc=None, ): # noinspection PyArgumentList super().__init__( default, default_factory, init, repr, hash, compare, metadata, kw_only, doc, ) self.load_alias = load_alias self.dump_alias = dump_alias self.env_vars = env_vars self.skip = skip self.path = path # In Python 3.10, dataclasses adds a new parameter to the :class:`Field` # constructor: `kw_only` # # Ref: https://docs.python.org/3.10/library/dataclasses.html#dataclasses.dataclass elif PY310_OR_ABOVE: # pragma: no cover # noinspection PyPep8Naming,PyShadowingBuiltins def Alias(*all, load=None, dump=None, env=None, skip=False, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING): all, load, dump, env = _normalize_alias_args(default, default_factory, all, load, dump, env) return Field( load, dump, env, skip, None, default, default_factory, init, repr, hash, compare, metadata, kw_only, ) # noinspection PyPep8Naming,PyShadowingBuiltins def AliasPath(*all, load=None, dump=None, skip=False, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING): all, load, dump = _normalize_alias_path_args(all, load, dump) return Field( load, dump, load, skip, all, default, default_factory, init, repr, hash, compare, metadata, kw_only, ) # noinspection PyShadowingBuiltins def skip_if_field( condition, *, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING ): if default is not MISSING and default_factory is not MISSING: raise ValueError("cannot specify both default and default_factory") if metadata is None: metadata = {} metadata["__skip_if__"] = condition return _Field( default, default_factory, init, repr, hash, compare, metadata, kw_only, ) class Field(_Field): __slots__ = ('load_alias', 'dump_alias', 'env_vars', 'skip', 'path') # noinspection PyShadowingBuiltins def __init__(self, load_alias, dump_alias, env_vars, skip, path, default, default_factory, init, repr, hash, compare, metadata, kw_only): super().__init__(default, default_factory, init, repr, hash, compare, metadata, kw_only) if path is not None: if isinstance(path, str): path = split_object_path(path) if path else (path, ) self.load_alias = load_alias self.dump_alias = dump_alias self.env_vars = env_vars self.skip = skip self.path = path else: # pragma: no cover # noinspection PyPep8Naming,PyShadowingBuiltins
[docs] def Alias(*all, load=None, dump=None, env=None, skip=False, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None): all, load, dump, env = _normalize_alias_args(default, default_factory, all, load, dump, env) return Field( load, dump, env, skip, None, default, default_factory, init, repr, hash, compare, metadata, )
# noinspection PyPep8Naming,PyShadowingBuiltins
[docs] def AliasPath(*all, load=None, dump=None, skip=False, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None): all, load, dump = _normalize_alias_path_args(all, load, dump) return Field( load, dump, load, skip, all, default, default_factory, init, repr, hash, compare, metadata, )
# noinspection PyShadowingBuiltins
[docs] def skip_if_field( condition, *, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None ): if default is not MISSING and default_factory is not MISSING: raise ValueError("cannot specify both default and default_factory") if metadata is None: metadata = {} metadata["__skip_if__"] = condition # noinspection PyArgumentList return _Field( default, default_factory, init, repr, hash, compare, metadata, )
[docs] class Field(_Field): __slots__ = ('load_alias', 'dump_alias', 'env_vars', 'skip', 'path') # noinspection PyArgumentList,PyShadowingBuiltins def __init__(self, load_alias, dump_alias, env_vars, skip, path, default, default_factory, init, repr, hash, compare, metadata): super().__init__(default, default_factory, init, repr, hash, compare, metadata) if path is not None: if isinstance(path, str): path = split_object_path(path) if path else (path,) self.load_alias = load_alias self.dump_alias = dump_alias self.env_vars = env_vars self.skip = skip self.path = path
Alias.__doc__ = """ Maps one or more JSON key names to a dataclass field. This function acts as an alias for ``dataclasses.field(...)``, with additional support for associating a field with one or more JSON keys. It customizes serialization and deserialization behavior, including handling keys with varying cases or alternative names. The mapping is case-sensitive; JSON keys must match exactly (e.g., ``myField`` will not match ``myfield``). If multiple keys are provided, the first one is used as the default for serialization. :param all: One or more JSON key names to associate with the dataclass field. :type all: str :param load: Key(s) to use for deserialization. Defaults to ``all`` if not specified. :type load: str | Sequence[str] | None :param dump: Key to use for serialization. Defaults to the first key in ``all``. :type dump: str | None :param skip: If ``True``, the field is excluded during serialization. Defaults to ``False``. :type skip: bool :param default: Default value for the field. Cannot be used with ``default_factory``. :type default: Any :param default_factory: Callable to generate the default value. Cannot be used with ``default``. :type default_factory: Callable[[], Any] :param init: Whether the field is included in the generated ``__init__`` method. Defaults to ``True``. :type init: bool :param repr: Whether the field appears in the ``__repr__`` output. Defaults to ``True``. :type repr: bool :param hash: Whether the field is included in the ``__hash__`` method. Defaults to ``None``. :type hash: bool :param compare: Whether the field is included in comparison methods. Defaults to ``True``. :type compare: bool :param metadata: Additional metadata for the field. Defaults to ``None``. :type metadata: dict :param kw_only: If ``True``, the field is keyword-only. Defaults to ``False``. :type kw_only: bool :return: A dataclass field with additional mappings to one or more JSON keys. :rtype: Field **Examples** **Example 1** -- Mapping multiple key names to a field:: from dataclasses import dataclass from dataclass_wizard import Alias, LoadMeta, fromdict @dataclass class Example: my_field: str = Alias('key1', 'key2', default="default_value") print(fromdict(Example, {'key2': 'a value!'})) #> Example(my_field='a value!') **Example 2** -- Skipping a field during serialization:: from dataclasses import dataclass from dataclass_wizard import Alias, JSONWizard @dataclass class Example(JSONWizard): my_field: str = Alias('key', skip=True) ex = Example.from_dict({'key': 'some value'}) print(ex) #> Example(my_field='a value!') assert ex.to_dict() == {} #> True """ AliasPath.__doc__ = """ Creates a dataclass field mapped to one or more nested JSON paths. This function acts as an alias for ``dataclasses.field(...)``, with additional functionality to associate a field with one or more nested JSON paths, including complex or deeply nested structures. The mapping is case-sensitive, meaning that JSON keys must match exactly (e.g., "myField" will not match "myfield"). Nested paths can include dot notations or bracketed syntax for accessing specific indices or keys. :param all: One or more nested JSON paths to associate with the dataclass field (e.g., ``a.b.c`` or ``a["nested"]["key"]``). :type all: PathType | str :param load: Path(s) to use for deserialization. Defaults to ``all`` if not specified. :type load: PathType | str | None :param dump: Path(s) to use for serialization. Defaults to ``all`` if not specified. :type dump: PathType | str | None :param skip: If True, the field is excluded during serialization. Defaults to False. :type skip: bool :param default: Default value for the field. Cannot be used with ``default_factory``. :type default: Any :param default_factory: A callable to generate the default value. Cannot be used with ``default``. :type default_factory: Callable[[], Any] :param init: Whether the field is included in the generated ``__init__`` method. Defaults to True. :type init: bool :param repr: Whether the field appears in the ``__repr__`` output. Defaults to True. :type repr: bool :param hash: Whether the field is included in the ``__hash__`` method. Defaults to None. :type hash: bool :param compare: Whether the field is included in comparison methods. Defaults to True. :type compare: bool :param metadata: Additional metadata for the field. Defaults to None. :type metadata: dict :param kw_only: If True, the field is keyword-only. Defaults to False. :type kw_only: bool :return: A dataclass field with additional mapping to one or more nested JSON paths. :rtype: Field **Examples** **Example 1** -- Mapping multiple nested paths to a field:: from dataclasses import dataclass from dataclass_wizard import AliasPath, fromdict @dataclass class Example: my_str: str = AliasPath('a.b.c.1', 'x.y["-1"].z', default="default_value") # Maps nested paths ('a', 'b', 'c', 1) and ('x', 'y', '-1', 'z') # to the `my_str` attribute. '-1' is treated as a literal string key, # not an index, for the second path. print(fromdict(Example, {'x': {'y': {'-1': {'z': 'some_value'}}}})) #> Example(my_str='some_value') **Example 2** -- Using Annotated:: from dataclasses import dataclass from typing import Annotated from dataclass_wizard import AliasPath, JSONWizard @dataclass class Example(JSONWizard): my_str: Annotated[str, AliasPath('my."7".nested.path.-321')] ex = Example.from_dict({'my': {'7': {'nested': {'path': {-321: 'Test'}}}}}) print(ex) #> Example(my_str='Test') """ Field.__doc__ = """ Alias to a :class:`dataclasses.Field`, but one which also represents a mapping of one or more JSON key names to a dataclass field. See the docs on the :func:`Alias` and :func:`AliasPath` for more info. """