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_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":
Deep Nesting#
The strategy applies at the top-level field. For three-level nesting like var.sub.key, the conflict is detected on var:
With nested_resolve_strategy="flat", the flat key APP__VAR__SUB__KEY wins.