Skip to content

ADR-002: Tree-Based Configuration Binding

⚠️ DEPRECATED: This decision has been superseded by ADR-010 (Unified Configuration). This document is kept for historical context only. Please refer to the User Guide.

Note: While the core concepts of tree-binding logic (ConfigResolver, ObjectGraphBuilder) and using @configured for nested structures remain valid, ADR-0010 unified the configuration system. The mechanism described here using a separate init(tree_config=...) argument is no longer current. Configuration sources (including tree sources like YamlTreeSource) are now passed to the configuration(...) builder, and the resulting ContextConfig object is passed to init(config=...). The @configured decorator now handles both flat and tree mapping via its mapping parameter.

Context

Basic configuration (via the old @configuration with ConfigSource — now removed) was suitable for flat key-value pairs but became cumbersome for complex, nested application settings common in modern microservices (e.g., configuring databases, caches, feature flags, external clients with nested properties). Manually parsing nested structures or using complex prefixes was error-prone and lacked type safety beyond simple primitives. We needed a way to map structured configuration files (like YAML or JSON) directly to Python object graphs (such as dataclasses).

Decision

We introduced a dedicated tree-binding system that remains the foundation for structured configuration:

  1. TreeSource Protocol: Sources that provide configuration as a nested Mapping (e.g., YamlTreeSource, JsonTreeSource). These are now passed to the unified configuration(...) builder (per ADR-0010).
  2. ConfigResolver: An internal component that loads, merges (sources are layered according to configuration(...) order), and interpolates (${ENV:VAR}, ${ref:path}) all TreeSources into a single, final configuration tree.
  3. ObjectGraphBuilder: An internal component that recursively maps a sub-tree (selected by a prefix) from the ConfigResolver onto a target Python type (usually a dataclass). It handles type coercion, nested objects, lists, dictionaries, Unions (with a Discriminator), and Enums.
  4. @configured(prefix="key", mapping="tree"|"auto") Decorator: A registration mechanism that tells the container to create a provider for the target type by using the ObjectGraphBuilder to map the configuration sub-tree found at prefix, when the mapping is determined to be "tree" (either explicitly or via "auto" detection).

How It Works (Post-ADR-0010)

  • Build configuration:
  • Call configuration(...) with the desired sources, including any tree sources such as YamlTreeSource and JsonTreeSource.
  • The builder produces a ContextConfig, which encapsulates the unified, merged configuration (flat and tree).
  • Register structured components:
  • Annotate target types (typically dataclasses) with @configured(prefix="...", mapping="tree"|"auto").
  • When mapping="auto", the system determines whether to apply tree or flat mapping based on the target type and the configuration structure.
  • Initialize the application:
  • Pass the constructed ContextConfig to init(config=...).
  • Providers registered via @configured are resolved by mapping configuration subtrees into the corresponding Python object graphs.

Mapping Rules (Summary)

  • Prefix selection: prefix identifies the subtree to map for a given provider.
  • Type coercion: Primitive types are coerced from strings/numbers as needed.
  • Nested objects: Nested dataclasses and classes are built recursively.
  • Collections: Lists and dictionaries are supported, including nested content.
  • Polymorphism: Union types are supported via a Discriminator to select the concrete type.
  • Enums: Enum values are mapped by name (and, where appropriate, by value).
  • Interpolation:
  • Environment: ${ENV:VAR} injects environment variable values.
  • References: ${ref:path} references other nodes within the configuration tree to avoid duplication.

Migration Notes (From Pre-ADR-0010)

  • Replace init(tree_config=...) with init(config=...).
  • Use the unified configuration(...) builder to supply sources (both flat and tree). Pass YamlTreeSource/JsonTreeSource directly to configuration(...).
  • Switch existing @configured(..., mapping="tree") usages to rely on the new unified handling. In many cases, mapping="auto" may be sufficient.

Consequences

Positive: - Enables highly structured, type-safe configuration. - Configuration structure directly mirrors dataclass definitions, improving clarity. - Supports common formats like YAML and JSON naturally. - Interpolation allows for dynamic values and avoids repetition. - Decouples components from the source of configuration (env, file, etc.). - Polymorphic configuration (Union + Discriminator) allows for flexible setup (e.g., selecting different cache backends via config).

Negative: - The split between flat and tree systems is resolved by ADR-0010, but users must understand the mapping rules (prefix, type coercion, discriminators, mapping parameter). - Adds optional dependencies for formats like YAML (pip install pico-ioc[yaml]).