headroomlabs-ai/headroom · error · TypeError

unserializable config value: {type(value).__name__}

Error message

unserializable config value: {type(value).__name__}

What it means

Raised by _json_default in config_compressor.py when JSON-serializing parsed TOML content encounters a value that is neither a TOML date/time nor a JSON-native type. The hook renders dt.datetime/dt.date/dt.time as ISO strings and treats everything else (e.g. tomllib's returned types that stray from the allowlist) as unserializable, failing loudly instead of emitting lossy config.

Source

Thrown at headroom/transforms/config_compressor.py:79

    """Parse TOML with the stdlib parser (or the tomli backport); None on error."""
    try:
        import tomllib
    except ModuleNotFoundError:  # pragma: no cover - Python < 3.11 only
        try:
            import tomli as tomllib  # type: ignore[no-redef]
        except ModuleNotFoundError:
            return None
    try:
        return cast("dict[str, Any]", tomllib.loads(content))
    except (tomllib.TOMLDecodeError, ValueError):
        return None


def _json_default(value: Any) -> str:
    """Render TOML date/time values as ISO strings; bail on anything else."""
    if isinstance(value, dt.datetime | dt.date | dt.time):
        return value.isoformat()
    raise TypeError(f"unserializable config value: {type(value).__name__}")


@dataclass
class ConfigCompressorConfig:
    """Configuration for structured-config compression."""

    # Emit the CCR-marked comment/blank elision tier. The router wires this
    # to its ccr_inject_marker setting; lossless mode turns it off.
    enable_ccr: bool = True
    # Bridge TOML array-of-tables to SmartCrusher csv-schema (Tier 3). Rides
    # CCR for recovery, so it only runs when enable_ccr is also on.
    enable_schema_fold: bool = True
    # Only adopt a result that is strictly smaller than the original.
    min_savings_chars: int = 1


@dataclass
class ConfigCompressionResult:

View on GitHub (pinned to 322425c43b)

Solutions

  1. Inspect the TypeError's type name to find the offending key in the TOML, and replace that value with a string/number/bool.
  2. If the type is legitimately serializable, extend _json_default with an isinstance branch for it (e.g. Decimal -> str(value)).
  3. Keep config files to plain TOML scalars, arrays, and tables.

Example fix

# before
def _json_default(value):
    if isinstance(value, dt.datetime | dt.date | dt.time):
        return value.isoformat()
    raise TypeError(f"unserializable config value: {type(value).__name__}")

# after — add a branch for the new type
def _json_default(value):
    if isinstance(value, dt.datetime | dt.date | dt.time):
        return value.isoformat()
    if isinstance(value, decimal.Decimal):
        return str(value)
    raise TypeError(f"unserializable config value: {type(value).__name__}")
Defensive patterns

Strategy: try-catch

Try / catch

try:
    payload = json.dumps(config_dict, default=_json_default)
except TypeError as e:
    if "unserializable config value" in str(e):
        locate_offending_key(config_dict)  # walk dict, test json.dumps per leaf
        raise ConfigError(f"config contains unsupported value: {e}") from e
    raise

Prevention

When it happens

Trigger: Serializing a TOML config that contains a value type _json_default does not whitelist — any non date/time custom object reaching json.dumps(default=_json_default) during structured-config compression.

Common situations: A TOML file with exotic inline values, or a code change that puts non-TOML objects into the dict before serialization; version drift in tomllib's returned types.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/fa6f39dcf42da113. Report an issue: GitHub.