Skip to content

Nested Resolve#

Flat-key sources (ENV, .env file, Docker secrets) store nested dataclasses as either a single JSON string or as separate flat keys:

# JSON form
APP__DATABASE={"host": "db.example.com", "port": "5432"}

# Flat form
APP__DATABASE__HOST=db.example.com
APP__DATABASE__PORT=5432

When both forms are present for the same field, dature needs to know which one to use. This is what nested_resolve_strategy and nested_resolve control.

The Problem#

"""The problem: both JSON and flat keys exist for the same nested field."""

import os
from dataclasses import dataclass

import dature

os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'
os.environ["APP__DATABASE__HOST"] = "flat-host"
os.environ["APP__DATABASE__PORT"] = "3306"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    database: Database


# Without nested_resolve_strategy, flat keys win by default
config = dature.load(dature.EnvSource(prefix="APP__"), schema=Config)

assert config.database.host == "flat-host"
assert config.database.port == 3306

By default, flat keys win (nested_resolve_strategy="flat"). This is usually what you want — flat keys are more specific and easier to override in CI/CD.

Global Strategy#

Set nested_resolve_strategy on Source to choose the source for all nested fields:

Strategy Behavior
"flat" (default) Prefer flat keys (APP__DATABASE__HOST) over JSON
"json" Prefer JSON value (APP__DATABASE) over flat keys

Note

The strategy only determines priority when both forms are present. If only one form exists, it is always used. For example, with nested_resolve_strategy="flat", a JSON value APP__DATABASE={"host": "x"} will still be parsed normally when there are no flat keys like APP__DATABASE__HOST.

"""Strategy is only a priority — if only one form exists, it is always used."""

import os
from dataclasses import dataclass

import dature

# Only JSON form, no flat keys
os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'

# Make sure no flat keys interfere
os.environ.pop("APP__DATABASE__HOST", None)
os.environ.pop("APP__DATABASE__PORT", None)


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    database: Database


# Even with strategy="flat", JSON is parsed because there are no flat keys
config = dature.load(
    dature.EnvSource(prefix="APP__", nested_resolve_strategy="flat"),
    schema=Config,
)

assert config.database.host == "json-host"
assert config.database.port == 5432
"""Global nested_resolve_strategy="flat" — use flat keys, ignore JSON."""

import os
from dataclasses import dataclass

import dature

os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'
os.environ["APP__DATABASE__HOST"] = "flat-host"
os.environ["APP__DATABASE__PORT"] = "3306"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    database: Database


config = dature.load(
    dature.EnvSource(prefix="APP__", nested_resolve_strategy="flat"),
    schema=Config,
)

assert config.database.host == "flat-host"
assert config.database.port == 3306
"""Global nested_resolve_strategy="json" — use JSON value, ignore flat keys."""

import os
from dataclasses import dataclass

import dature

os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'
os.environ["APP__DATABASE__HOST"] = "flat-host"
os.environ["APP__DATABASE__PORT"] = "3306"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    database: Database


config = dature.load(
    dature.EnvSource(prefix="APP__", nested_resolve_strategy="json"),
    schema=Config,
)

assert config.database.host == "json-host"
assert config.database.port == 5432

Per-Field Strategy#

Use nested_resolve to set different strategies for individual fields:

"""Per-field nested_resolve — different strategies for different fields."""

import os
from dataclasses import dataclass

import dature

os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'
os.environ["APP__DATABASE__HOST"] = "flat-host"
os.environ["APP__DATABASE__PORT"] = "3306"
os.environ["APP__CACHE"] = '{"host": "json-cache", "ttl": "60"}'
os.environ["APP__CACHE__HOST"] = "flat-cache"
os.environ["APP__CACHE__TTL"] = "120"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Cache:
    host: str
    ttl: int


@dataclass
class Config:
    database: Database
    cache: Cache


# database uses JSON, cache uses flat keys
config = dature.load(
    dature.EnvSource(
        prefix="APP__",
        nested_resolve={
            "json": (dature.F[Config].database,),
            "flat": (dature.F[Config].cache,),
        },
    ),
    schema=Config,
)

assert config.database.host == "json-host"
assert config.database.port == 5432
assert config.cache.host == "flat-cache"
assert config.cache.ttl == 120

Per-Field Overrides Global#

When both nested_resolve_strategy and nested_resolve are set, per-field takes priority:

"""Per-field nested_resolve overrides global nested_resolve_strategy."""

import os
from dataclasses import dataclass

import dature

os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'
os.environ["APP__DATABASE__HOST"] = "flat-host"
os.environ["APP__DATABASE__PORT"] = "3306"
os.environ["APP__CACHE"] = '{"host": "json-cache", "ttl": "60"}'
os.environ["APP__CACHE__HOST"] = "flat-cache"
os.environ["APP__CACHE__TTL"] = "120"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Cache:
    host: str
    ttl: int


@dataclass
class Config:
    database: Database
    cache: Cache


# Global: "flat" for everything, but database overridden to "json"
config = dature.load(
    dature.EnvSource(
        prefix="APP__",
        nested_resolve_strategy="flat",
        nested_resolve={"json": (dature.F[Config].database,)},
    ),
    schema=Config,
)

assert config.database.host == "json-host"  # per-field override wins
assert config.database.port == 5432
assert config.cache.host == "flat-cache"  # global strategy
assert config.cache.ttl == 120

All Flat-Key Sources#

The mechanism works identically across all flat-key sources:

"""Global nested_resolve_strategy="json" — use JSON value, ignore flat keys."""

import os
from dataclasses import dataclass

import dature

os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'
os.environ["APP__DATABASE__HOST"] = "flat-host"
os.environ["APP__DATABASE__PORT"] = "3306"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    database: Database


config = dature.load(
    dature.EnvSource(prefix="APP__", nested_resolve_strategy="json"),
    schema=Config,
)

assert config.database.host == "json-host"
assert config.database.port == 5432
"""nested_resolve_strategy with .env file source."""

from dataclasses import dataclass
from pathlib import Path

import dature

SOURCES_DIR = Path(__file__).parent / "sources"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    database: Database


config = dature.load(
    dature.EnvFileSource(
        file=SOURCES_DIR / "nested_resolve.env",
        prefix="APP__",
        nested_resolve_strategy="json",
    ),
    schema=Config,
)

assert config.database.host == "json-host"
assert config.database.port == 5432
nested_resolve.env
APP__DATABASE={"host": "json-host", "port": "5432"}
APP__DATABASE__HOST=flat-host
APP__DATABASE__PORT=3306
"""nested_resolve_strategy with Docker secrets source."""

from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory

import dature


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    database: Database


with TemporaryDirectory() as secrets_dir:
    secrets_path = Path(secrets_dir)
    (secrets_path / "database").write_text(
        '{"host": "json-host", "port": "5432"}',
    )
    (secrets_path / "database__host").write_text("flat-host")
    (secrets_path / "database__port").write_text("3306")

    config = dature.load(
        dature.DockerSecretsSource(
            dir_=secrets_path,
            nested_resolve_strategy="json",
        ),
        schema=Config,
    )

    assert config.database.host == "json-host"
    assert config.database.port == 5432

Error Messages#

When a conflict is resolved, error messages point to the chosen source. With nested_resolve_strategy="json":

Config loading errors (1)

  [database.host]  Missing required field
   └── ENV 'APP__DATABASE' = '{"port": "5432"}'

With nested_resolve_strategy="flat":

Config loading errors (1)

  [database.host]  Missing required field
   └── ENV 'APP__DATABASE__HOST'

Deep Nesting#

The strategy applies at the top-level field. For three-level nesting like var.sub.key, the conflict is detected on var:

APP__VAR={"sub": {"key": "from_json"}}
APP__VAR__SUB__KEY=from_flat

With nested_resolve_strategy="flat", the flat key APP__VAR__SUB__KEY wins.