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#
```python
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__"), 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.
import os
from dataclasses import dataclass
import dature
os.environ["APP__DATABASE"] = '{"host": "json-host", "port": "5432"}'
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
config = dature.load(
dature.EnvSource(prefix="APP__", nested_resolve_strategy="flat"),
schema=Config,
)
# priority flat values not found -> parsing JSON
assert config.database.host == "json-host"
assert config.database.port == 5432
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(
# priority - flat values
dature.EnvSource(prefix="APP__", nested_resolve_strategy="flat"),
schema=Config,
)
assert config.database.host == "flat-host"
assert config.database.port == 3306
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(
# priority - json values
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:
```python
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
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:
```python
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
config = dature.load(
dature.EnvSource(
prefix="APP__",
# global strategy
nested_resolve_strategy="flat",
# exclusion for db
nested_resolve={"json": (dature.F[Config].database,)},
),
schema=Config,
)
assert config.database.host == "json-host"
assert config.database.port == 5432
assert config.cache.host == "flat-cache"
assert config.cache.ttl == 120
```
All Flat-Key Sources#
The mechanism works identically across all flat-key sources:
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(
# priority - json values
dature.EnvSource(prefix="APP__", nested_resolve_strategy="json"),
schema=Config,
)
assert config.database.host == "json-host"
assert config.database.port == 5432
from dataclasses import dataclass
import dature
@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
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.