Type Hooks

Note

If you want to customize serialization for specific fields (rather than a type everywhere it appears), see Serializer Hooks.

Type hooks let you extend Dataclass Wizard to support custom or unsupported types, by defining how a type is:

  • loaded (parsed) from JSON/dicts into a Python object, and

  • dumped (serialized) back into JSON-compatible data.

This is the recommended way to add support for types such as ipaddress.IPv4Address, pathlib.Path, custom IDs, and other domain types.

When to use type hooks

Use type hooks when:

  • a type is not supported out of the box and you want a clean, reusable solution

  • you want consistent behavior for a type across many dataclasses

  • you want to avoid sprinkling per-field logic throughout your models

If you only need special handling for a single field (or a small subset of fields), prefer Serializer Hooks.

Quick start: register a type

The simplest approach is to register a type and rely on sensible defaults:

  • load: Type(value)

  • dump: str(value)

Example: ipaddress.IPv4Address

from __future__ import annotations  # Remove if Python 3.10+

from ipaddress import IPv4Address

from dataclass_wizard import DataclassWizard, register_type


class Foo(DataclassWizard):
    # DataclassWizard auto-applies @dataclass to subclasses
    c: IPv4Address | None = None


register_type(Foo, IPv4Address)

foo = Foo.from_dict({"c": "127.0.0.1"})
assert foo.c == IPv4Address("127.0.0.1")
assert foo.to_dict() == {"c": "127.0.0.1"}

If you omit the registration, you will get an error indicating the type is not supported (and it should indicate whether the failure occurred during load or dump).

No Inheritance Needed

Type hooks also work without subclassing DataclassWizard or JSONWizard. This is useful when you prefer plain dataclasses and use the functional API (fromdict/asdict).

from __future__ import annotations  # Remove if Python 3.10+

from dataclasses import dataclass
from ipaddress import IPv4Address

from dataclass_wizard import asdict, fromdict, register_type


@dataclass
class Foo:
    b: bytes = b""
    s: str | None = None
    c: IPv4Address | None = None


# Register IPv4Address with default hooks (load=IPv4Address, dump=str)
register_type(Foo, IPv4Address)

data = {"b": "AAAA", "c": "127.0.0.1", "s": "foobar"}

foo = fromdict(Foo, data)
assert asdict(foo) == data
assert asdict(fromdict(Foo, asdict(foo))) == data

Registering custom load and dump functions

You can override the defaults by providing custom functions. In general:

  • The load function should return the target type (or object).

  • The dump function must return a JSON-serializable value (str, int, float, bool, None, list, dict).

from decimal import Decimal, ROUND_HALF_UP

from dataclass_wizard import DataclassWizard, register_type


def load_decimal(v):
    # Normalize all decimals to 2 decimal places on load
    return Decimal(v).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)


def dump_decimal(v: Decimal):
    # Serialize as string to preserve precision
    return str(v)


class Invoice(DataclassWizard):
    total: Decimal


# Override the built-in Decimal behavior
register_type(Invoice, Decimal, load=load_decimal, dump=dump_decimal)

invoice = Invoice.from_dict({'total': '1.235'})
print(invoice)  # Invoice(total=Decimal('1.24'))
print(invoice.to_dict())  # {'total': '1.24'}

Code generation hooks (advanced)

Starting in v1.x, you may choose to provide codegen hooks. These hooks accept (TypeInfo, Extras) and return a string expression (or TypeInfo) used by the v1 compiler.

This is useful if you need to integrate directly with the v1 compilation pipeline.

Note

Most users should start with register_type() and only use codegen hooks when needed.

Example: IPv4Address with codegen hooks

from ipaddress import IPv4Address

from dataclass_wizard import DataclassWizard
from dataclass_wizard._models import TypeInfo, Extras


def load_to_ipv4_address(tp: TypeInfo, extras: Extras) -> TypeInfo | str:
    # Wrap the value expression using the type's constructor
    return tp.wrap(tp.v(), extras)


def dump_from_ipv4_address(tp: TypeInfo, extras: Extras) -> str:
    # Dump an IPv4Address by converting to string
    return f"str({tp.v()})"


class Foo(DataclassWizard):
    class Meta(DataclassWizard.Meta):
        type_to_load_hook = {IPv4Address: load_to_ipv4_address}
        type_to_dump_hook = {IPv4Address: dump_from_ipv4_address}

    c: IPv4Address | None = None


foo = Foo.from_dict({"c": "127.0.0.1"})
assert foo.to_dict() == {"c": "127.0.0.1"}

Declaring hooks via Meta

If you prefer a declarative style, you can set hooks in Meta.

from ipaddress import IPv4Address

from dataclass_wizard import DataclassWizard, register_type


# DataclassWizard auto-applies @dataclass to subclasses
class Foo(DataclassWizard):
    c: IPv4Address | None = None


register_type(Foo, IPv4Address)

If you want to avoid method calls entirely, you can also register via Meta. (Exact configuration options may vary depending on the engine you use.)

from __future__ import annotations  # Remove if Python 3.10+

from dataclasses import dataclass
from ipaddress import IPv4Address

from dataclass_wizard import JSONWizard


@dataclass
class Foo(JSONWizard):
    class Meta(JSONWizard.Meta):
        # Equivalent of register_type(Foo, IPv4Address)
        # Defaults: load=IPv4Address, dump=str
        type_to_load_hook = {IPv4Address: IPv4Address}
        type_to_dump_hook = {IPv4Address: str}

    c: IPv4Address | None = None


assert Foo.from_dict({'c': '1.2.3.4'}).c == IPv4Address('1.2.3.4')  # True

Enum example: load & dump by name

By default, enums are typically loaded and dumped using their value. If you prefer to load and dump enums by their name instead, you can override the default behavior using type hooks.

from enum import Enum

from dataclass_wizard import DataclassWizard, register_type


class MyEnum(Enum):
    NAME_1 = 'one'
    NAME_2 = 'two'


def load_enum_by_name(v):
    # Input example: 'NAME 1' -> MyEnum.NAME_1
    return MyEnum[v.replace(' ', '_')]


def dump_enum_by_name(e: MyEnum):
    # Output example: MyEnum.NAME_1 -> 'NAME 1'
    return e.name.replace('_', ' ')


class MyClass(DataclassWizard):
    my_str: str
    my_enum: MyEnum


# Override the built-in Enum behavior
register_type(MyClass, MyEnum, load=load_enum_by_name, dump=dump_enum_by_name)

data = {'my_str': 'my string', 'my_enum': 'NAME 1'}

c = MyClass.from_dict(data)
assert c.my_enum is MyEnum.NAME_1
assert c.to_dict() == data

Runtime vs. codegen hooks

Dataclass Wizard supports two styles of hooks:

Runtime hooks

Regular Python callables used at runtime.

  • load hook: fn(value) -> object

  • dump hook: fn(object) -> json_value

Codegen hooks

Functions used by the v1 compiler.

  • hook: fn(TypeInfo, Extras) -> str | TypeInfo

If you provide a codegen hook, it must return a valid Python expression as a string, referencing any required types/functions that are in scope for the generated code.

Errors and troubleshooting

Unsupported type errors

If a type is unsupported, Dataclass Wizard will raise a parse/serialization error. The error should indicate:

  • the field name

  • whether the error occurred during load or dump

  • the unsupported type

  • a resolution hint (register a type hook)

If your dump hook returns a non-JSON value

Ensure your dump hook returns JSON-compatible primitives (or nested structures composed of primitives).

If you see name errors in generated code

Your codegen hook must reference names that are in scope for the generated function. Prefer builtins (like str) or ensure the type/function is available to the compiler (via locals injection, if applicable).

See also