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
@dataclassfields.Each un-annotated inner class whose
__qualname__matchesOuterClass.InnerNameis recursively processed with@deep_dataclassand promoted to a field whosedefault_factoryconstructs a default instance. The result is a fully valid@dataclasscompatible withdataclasses.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;Nonewhen called with keyword arguments.coerce_dicts (
bool, defaultTrue) –When
True, injects a__post_init__hook that recursively coerces anydictargument 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 toT.List[T]— coerces each list element toT.Tuple[T, ...]— coerces tuple elements to their declared types.
A user-defined
__post_init__is preserved and called after coercion. Incompatible withinit=False.autosnake (
bool, defaultFalse) – WhenTrue, PascalCase inner class names are converted to snake_case field names (e.g.class AdamSolverbecomes fieldadam_solver). The original PascalCase name is retained as a class attribute so thateval(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
@dataclasswith all inner classes promoted to fields. If the class was already a@dataclasswhen decorated, its__init__is wrapped to add coercion rather than re-applying@dataclass.- Return type:
type- Raises:
TypeError – If
init=Falseis passed together withcoerce_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 withfield(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 aList[Plugin]field — that should not appear on their own.Annotated names (
name: typeorname: 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 viavars(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
autosnakeconverts 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
@auxiliarymarks 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
@dataclasstypes 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 aList[T], one arm of aUnion[A, B], or the value type of aDict[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_dataclassand converted to a@dataclassautomatically.- Returns:
The same class, with the
__deep_dataclass_auxiliary__attribute set toTrue.- Return type:
type
Notes
@auxiliarymust 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
@auxiliaryas 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
@auxiliaryas aUnionvariant:>>> 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
objectfor 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@dataclassor@deep_dataclassclass.strict (
bool, defaultFalse) –When
False(default), only fields without a default value and not typed asOptionalare 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 asOptional. Use this when you want to enforce that all fields are always present.allow_additional_properties (
bool, defaultFalse) –When
False(default),"additionalProperties": falseis added to the schema, rejecting any keys not declared as fields. This is the right choice for closed schemas such asUnionvariant discrimination.When
True, extra keys are silently accepted. Useful when the dataclass represents a partial view of a larger document.
- Returns:
A JSON Schema
objectwith 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"Falseunless allow_additional_properties isTrue.
- Return type:
dict
Notes
The validate-then-construct pattern works end-to-end with
@deep_dataclassbecause 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=Truerequires 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=Trueaccepts extra keys:>>> open_schema = to_json_schema(Config, allow_additional_properties=True) >>> jsonschema.validate({"epochs": 10, "unknown_key": 42}, open_schema)
Literalfields 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
ValueErrorif applied to a non-dataclass. RaisesTypeErrorif 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 toto_json_schema(). WhenTrue, fields without defaults are treated as required in the schema.enabled (
bool) – WhenFalsethe 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 toto_json_schema(). WhenTrue, fields without defaults are treated as required in the schema.
- Returns:
Falseif cls is not a dataclass, if jsonschema is not installed, or if validation fails.- Return type:
bool