Skip to content

ADR-012: Type Coercion

Status

  • Proposed by: Ryan on 2026-01-07
  • Accepted on: 2026-01-07

Context

Resolvers often return string values (e.g., ${env:PORT} returns "8080"), but the schema or application may expect a different type (e.g., integer). We need to decide how holoconf handles type mismatches between resolved values and expected types.

This affects: - Schema validation (ADR-007) - Resolver return values (ADR-002) - Serialization output (ADR-009)

Alternatives Considered

Alternative 1: No Coercion (Strict Types)

Resolved values must match the expected type exactly. Type mismatches raise errors.

# Schema expects integer
port:
  type: integer

# Config
port: ${env:PORT}  # Returns "8080" (string) → ValidationError
  • Pros: Explicit, no surprises, forces correct resolver implementation
  • Cons: Env vars are always strings, would require wrapper resolvers or schema workarounds

Alternative 2: Automatic Coercion (Implicit)

Automatically coerce values to match schema types when possible.

# Schema expects integer
port:
  type: integer

# Config
port: ${env:PORT}  # Returns "8080" → coerced to 8080 (integer)
  • Pros: Convenient, works naturally with env vars
  • Cons: Can hide bugs, unexpected behavior if coercion fails silently

Alternative 3: Schema-Driven Coercion (Explicit)

Coercion only happens when a schema is present and defines the expected type. Without a schema, values retain their resolved type.

# With schema expecting integer: "8080" → 8080
# Without schema: "8080" stays "8080"
  • Pros: Predictable, schema controls behavior, no coercion surprises without schema
  • Cons: Behavior differs with/without schema

Open Questions (Proposal Phase)

All resolved - see Decision section.

Next Steps (Proposal Phase)

  • [ ] Implement coercion in holoconf-core
  • [ ] Test edge cases (empty strings, whitespace, locale-specific numbers)
  • [ ] Document resolver-driven coercion option

Decision

Schema-Driven Coercion with Resolver Override

  • Coercion is primarily schema-driven: when a schema specifies an expected type, holoconf attempts to coerce the resolved value
  • Resolvers may also perform their own coercion before returning values (resolver-driven)
  • Schema coercion applies after resolver returns, so resolvers returning already-typed values skip coercion
  • Boolean coercion is strict: only "true" and "false" (case-insensitive), not "1"/"0" or "yes"/"no"
  • No string-to-array coercion; use a built-in resolver for splitting if needed

Design

Schema-Driven Coercion

Coercion happens automatically when: 1. A schema is provided 2. The schema specifies an expected type 3. The resolved value doesn't match the expected type 4. A valid coercion rule exists

Resolver-Driven Coercion

Resolvers may return already-typed values:

class SmartEnvResolver(Resolver):
    def resolve(self, key: str) -> ResolvedValue:
        value = os.environ.get(key)
        # Resolver does its own coercion
        if value.isdigit():
            return ResolvedValue(value=int(value), sensitive=False)
        return ResolvedValue(value=value, sensitive=False)

When a resolver returns a typed value: - If it matches the schema type, no coercion needed - If it doesn't match, schema coercion is attempted - This allows resolvers to optimize for common cases

Coercion Rules

From To Rule Example
string integer Parse as integer, fail if not valid "8080" → 8080, "abc" → error
string number Parse as float "3.14" → 3.14
string boolean "true"/"false" only (case-insensitive) "true" → true, "1" → error
string array No coercion (use split resolver) error
string object No coercion error
integer number Widen to float 8080 → 8080.0
any string No coercion needed (strings accept any) value unchanged

Coercion Failure Behavior

When coercion fails, raise TypeValidationError with context:

TypeValidationError: Cannot coerce value to expected type
  Path: database.port
  Expected: integer
  Got: string ("not-a-number")
  Resolved from: ${env:DB_PORT}
  Help: Ensure DB_PORT environment variable contains a valid integer

No-Schema Behavior

Without a schema, no coercion occurs. Values retain their resolved type: - ${env:PORT} → "8080" (string) - Accessing config.port returns "8080" (string)

Opt-Out

If schema specifies type: string, no coercion occurs even if the value looks like a number.

Rationale

  • Schema-driven coercion is predictable - users know when coercion happens (only with schema)
  • Resolver override enables optimization - resolvers can return typed values to skip coercion
  • Strict boolean parsing prevents bugs - "1" or "yes" silently becoming true can hide errors
  • No string-to-array keeps things simple - complex parsing should be explicit via resolvers

Trade-offs Accepted

  • Stricter boolean coercion may require users to update configs using "1"/"0", in exchange for predictable behavior
  • No implicit array splitting requires explicit resolver usage, in exchange for avoiding ambiguous parsing
  • Schema required for coercion means unvalidated configs keep string types, in exchange for explicit behavior

Migration

N/A - This is the initial architecture decision.

Consequences

  • Positive: Predictable coercion, resolvers can optimize, clear separation of concerns
  • Negative: Users expecting "1" → true will need to adjust
  • Neutral: Resolvers have flexibility to do their own coercion if desired