API Reference

deep_dataclass

deep_dataclasses.deep_dataclass(cls=None, *, coerce_dicts=True, autosnake=False, **dataclass_kwargs)[source]

Class decorator that promotes inner class definitions to @dataclass fields.

Each un-annotated inner class whose __qualname__ matches OuterClass.InnerName is recursively processed with @deep_dataclass and promoted to a field whose default_factory constructs a default instance. The result is a fully valid @dataclass compatible with dataclasses.asdict, dataclasses.fields, repr, ==, and all standard dataclass tooling.

Can be used with or without arguments:

@deep_dataclass
@deep_dataclass()
@deep_dataclass(autosnake=True, frozen=True)
Parameters:
  • cls (type, optional) – The class being decorated. Supplied automatically by Python when the decorator is used without parentheses; None when called with keyword arguments.

  • coerce_dicts (bool, default True) –

    When True, injects a __post_init__ hook that recursively coerces any dict argument into its declared dataclass type. Coercion descends through:

    • SomeDataclass — direct coercion.

    • Optional[T] — coerces the inner type when the value is a dict.

    • Union[A, B, ...] — selects the dataclass variant whose field names cover all keys in the dict, preferring the variant with the fewest unfilled fields (exact match wins).

    • Dict[K, T] — coerces each dict value to T.

    • List[T] — coerces each list element to T.

    • Tuple[T, ...] — coerces tuple elements to their declared types.

    A user-defined __post_init__ is preserved and called after coercion. Incompatible with init=False.

  • autosnake (bool, default False) – When True, PascalCase inner class names are converted to snake_case field names (e.g. class AdamSolver becomes field adam_solver). The original PascalCase name is retained as a class attribute so that eval(repr(obj)) round-trips correctly.

  • **dataclass_kwargs – Forwarded verbatim to dataclasses.dataclass() (e.g. frozen=True, order=True, eq=False).

Returns:

The decorated class as a @dataclass with all inner classes promoted to fields. If the class was already a @dataclass when decorated, its __init__ is wrapped to add coercion rather than re-applying @dataclass.

Return type:

type

Raises:
  • TypeError – If init=False is passed together with coerce_dicts=True.

  • TypeError – If a mutable default value is not a list, dict, or set.

  • TypeError – If a list, dict, or set default contains non-primitive values.

  • TypeError – If a subclass adds mandatory fields after a parent with defaulted fields.

Notes

  • Mutable defaults (list, dict, set) are automatically wrapped with field(default_factory=...). Empty containers use the type itself as the factory; non-empty containers use a lambda. Elements must be primitive types (int, str, float, bool, None).

  • Inner classes decorated with auxiliary() are processed and kept as class attributes but are not promoted to standalone fields. Use this for shared type definitions — for example, an element type for a List[Plugin] field — that should not appear on their own.

  • Annotated names (name: type or name: type = default) are never treated as inner classes, even when they hold a type value.

  • Fields without defaults (mandatory annotations) are placed before fields with defaults, satisfying the restriction imposed by @dataclass.

  • from __future__ import annotations (PEP 563) is fully compatible; inner-class type hints are resolved via vars(cls) at coercion time.

Examples

Basic nested configuration:

>>> @deep_dataclass
... class Config:
...     class Optimizer:
...         lr: float = 1e-3
...         momentum: float = 0.9
...     class Scheduler:
...         step_size: int = 10
...         gamma: float = 0.1
...     epochs: int = 100
>>> Config().Optimizer.lr
0.001
>>> Config().epochs
100

Mutable defaults are wrapped automatically:

>>> cfg = Config(Optimizer={"lr": 0.01})
>>> cfg.Optimizer.lr
0.01
>>> isinstance(cfg.Optimizer, Config.Optimizer)
True

Round-trip through asdict:

>>> from dataclasses import asdict
>>> Config(**asdict(cfg)) == cfg
True

autosnake converts PascalCase inner class names to snake_case fields:

>>> @deep_dataclass(autosnake=True)
... class Model:
...     class TransformerEncoder:
...         num_layers: int = 6
>>> Model().transformer_encoder.num_layers
6
>>> Model().TransformerEncoder.num_layers   # PascalCase alias preserved
6

@auxiliary marks an inner class as a type helper without creating a field for it:

>>> from typing import List
>>> from dataclasses import field
>>> @deep_dataclass
... class Experiment:
...     tags: list = []
...     scores: list = [1, 2, 3]
...     meta: dict = {}
>>> Experiment().tags is Experiment().tags
False

auxiliary

deep_dataclasses.auxiliary(cls)[source]

Mark a class as a type-only helper inside a @deep_dataclass.

Decorated inner classes are converted to proper @dataclass types and remain accessible as class attributes, but are not promoted to standalone fields on the enclosing class. Use this when an inner class is needed purely as a type (e.g. as the element type of a List[T], one arm of a Union[A, B], or the value type of a Dict[K, V]) and should not appear as a default-constructed field in its own right.

Parameters:

cls (type) – The inner class to mark as auxiliary. Must be a plain class; it will be processed by @deep_dataclass and converted to a @dataclass automatically.

Returns:

The same class, with the __deep_dataclass_auxiliary__ attribute set to True.

Return type:

type

Notes

  • @auxiliary must be applied before the enclosing class is decorated with @deep_dataclass, i.e. as an inner decorator inside the class body.

  • The processed class is still accessible on the enclosing class under its original name, so it can be used in type annotations and passed to isinstance.

Examples

Using @auxiliary as an element type for a list field:

>>> from dataclasses import field
>>> from typing import List
>>> from deep_dataclasses import deep_dataclass, auxiliary
>>>
>>> @deep_dataclass
... class Pipeline:
...     @auxiliary
...     class Stage:
...         name: str = ""
...         enabled: bool = True
...     stages: List[Stage] = field(default_factory=list)
>>>
>>> p = Pipeline(stages=[{"name": "preprocess"}, {"name": "train"}])
>>> p.stages[0].name
'preprocess'
>>> "Stage" in {f.name for f in dataclasses.fields(Pipeline)}
False

Using @auxiliary as a Union variant:

>>> from typing import Union
>>>
>>> @deep_dataclass
... class Config:
...     @auxiliary
...     class TrainMode:
...         lr: float = 1e-3
...     @auxiliary
...     class TestMode:
...         metric: str = "accuracy"
...     mode: Union[TrainMode, TestMode] = field(default_factory=TrainMode)
>>>
>>> isinstance(Config(mode={"lr": 0.01}).mode, Config.TrainMode)
True

to_json_schema

deep_dataclasses.to_json_schema(cls, strict=False, allow_additional_properties=False)[source]

Generate a JSON Schema object for a dataclass.

Recursively converts the field types of cls to their JSON Schema equivalents. The resulting schema can be used directly with any JSON Schema validator (e.g. jsonschema.validate).

The following Python types are supported:

  • Primitives: bool, int, float, str, None / type(None)

  • Collections: List[T], Tuple[T, ...], Tuple[T1, T2, ...], Set[T], FrozenSet[T], Dict[K, V]

  • Composites: Optional[T], Union[A, B, ...], Literal[...]

  • Nested dataclasses (recursed automatically)

  • typing.Any (no constraint)

Parameters:
  • cls (type) – A @dataclass or @deep_dataclass class.

  • strict (bool, default False) –

    When False (default), only fields without a default value and not typed as Optional are listed under "required". This allows partial dicts to validate successfully as long as omitted fields have defaults.

    When True, every field — even those with defaults — is added to "required" unless explicitly typed as Optional. Use this when you want to enforce that all fields are always present.

  • allow_additional_properties (bool, default False) –

    When False (default), "additionalProperties": false is added to the schema, rejecting any keys not declared as fields. This is the right choice for closed schemas such as Union variant discrimination.

    When True, extra keys are silently accepted. Useful when the dataclass represents a partial view of a larger document.

Returns:

A JSON Schema object with the following keys:

"type"

Always "object".

"properties"

A dict mapping each field name to its type schema, including a "default" key when the field has a default value or factory.

"required"

List of field names that must be present (only included when at least one field is required).

"additionalProperties"

False unless allow_additional_properties is True.

Return type:

dict

Notes

The validate-then-construct pattern works end-to-end with @deep_dataclass because construction coerces nested dicts to their declared types. With plain @dataclass, validation succeeds but nested dicts are not coerced, so the constructed object may contain raw dicts in place of typed fields.

Examples

Basic usage — validate a raw dict before constructing:

>>> import jsonschema
>>> from dataclasses import dataclass
>>> from deep_dataclasses import deep_dataclass, to_json_schema
>>>
>>> @deep_dataclass
... class Config:
...     class Optimizer:
...         lr: float = 1e-3
...         momentum: float = 0.9
...     epochs: int = 100
>>>
>>> schema = to_json_schema(Config)
>>> jsonschema.validate({"Optimizer": {"lr": 0.01}, "epochs": 50}, schema)
>>> cfg = Config(**{"Optimizer": {"lr": 0.01}, "epochs": 50})
>>> cfg.Optimizer.lr
0.01

strict=True requires every field to be present, even those with defaults:

>>> strict = to_json_schema(Config, strict=True)
>>> jsonschema.validate({"epochs": 50}, strict)  # raises — Optimizer missing
Traceback (most recent call last):
    ...
jsonschema.exceptions.ValidationError: 'Optimizer' is a required property

allow_additional_properties=True accepts extra keys:

>>> open_schema = to_json_schema(Config, allow_additional_properties=True)
>>> jsonschema.validate({"epochs": 10, "unknown_key": 42}, open_schema)

Literal fields are enforced via "enum":

>>> from typing import Literal
>>>
>>> @dataclass
... class Run:
...     device: Literal["cpu", "cuda"] = "cpu"
>>>
>>> jsonschema.validate({"device": "tpu"}, to_json_schema(Run))
Traceback (most recent call last):
    ...
jsonschema.exceptions.ValidationError: 'tpu' is not valid under any of the given schemas

deep_dataclasses.extras

deep_dataclasses.extras.validate_defaults(cls=None, *, strict=True, enabled=True)[source]

Decorator that validates the default instance of a dataclass at definition time.

Raises ValueError if applied to a non-dataclass. Raises TypeError if the default instance fails schema validation.

Can be used with or without arguments:

@validate_defaults
@deep_dataclass
class Cfg:
    x: int = 0

@validate_defaults(strict=False)
@deep_dataclass
class Cfg:
    x: int = 0
Parameters:
  • cls (type, optional) – The class being decorated (supplied automatically when used without parentheses).

  • strict (bool) – Passed to to_json_schema(). When True, fields without defaults are treated as required in the schema.

  • enabled (bool) – When False the decorator is a no-op and returns the class unchanged. Defaults to __debug__, so validation is skipped automatically when Python is run with -O / PYTHONOPTIMIZE=1.

Raises:
  • ValueError – If cls is not a dataclass (only raised when enabled is True).

  • TypeError – If the default instance of cls fails JSON schema validation.

  • ImportError – If jsonschema is not installed and enabled is True.

deep_dataclasses.extras.dataclass_default_can_be_validated(cls, strict=True)[source]

Return True if the default instance of cls passes its own JSON schema.

Parameters:
  • cls (type) – A dataclass (or deep_dataclass) to check.

  • strict (bool) – Passed to to_json_schema(). When True, fields without defaults are treated as required in the schema.

Returns:

False if cls is not a dataclass, if jsonschema is not installed, or if validation fails.

Return type:

bool