RyanCodrai/turbovec · error · TypeError

side-car key {key!r} at {_crumb_path(entry)} is {type(key)._

Error message

side-car key {key!r} at {_crumb_path(entry)} is {type(key).__name__}, not str. JSON object keys are strings, so writing it would stringify the key and silently merge it with any existing {str(key)!r} key, losing data on reload. Convert the key to a str before saving.

What it means

Before atomically writing the JSON side-car, atomic_save runs _check_json_faithful to prove the payload survives a JSON round-trip without data loss. JSON object keys are always strings, so a dict with a non-str key (int, tuple, None...) would be silently stringified by json.dump and could collide with an existing string key of the same text. The check raises TypeError at the offending path (described by _crumb_path) with instructions.

Source

Thrown at turbovec-python/python/turbovec/_persist.py:309

    # children's parent link, so the walk costs one tuple per node and
    # builds no path strings. Eagerly formatting `f"{path}[{key!r}]"` for
    # every node cost ~18% of the whole validation pass on a 200k-doc
    # payload, all of it to produce strings that are thrown away unless a
    # save fails. `_crumb_path` reconstructs the path from the links on
    # the failure path only.
    root = (payload, None, None)
    stack = [root]
    seen: set[int] = set()
    while stack:
        entry = stack.pop()
        obj = entry[0]
        if isinstance(obj, dict):
            if id(obj) in seen:
                continue
            seen.add(id(obj))
            for key, value in obj.items():
                if not isinstance(key, str):
                    raise TypeError(
                        f"side-car key {key!r} at "
                        f"{_crumb_path(entry)} is {type(key).__name__}, not "
                        f"str. JSON object keys are strings, so writing it "
                        f"would stringify the key and silently merge it with "
                        f"any existing {str(key)!r} key, losing data on "
                        f"reload. Convert the key to a str before saving."
                    )
                stack.append((value, entry, key))
        elif isinstance(obj, (list, tuple)):
            if id(obj) in seen:
                continue
            seen.add(id(obj))
            for i, value in enumerate(obj):
                stack.append((value, entry, i))
        elif isinstance(obj, float) and not math.isfinite(obj):
            if math.isnan(obj):
                token = "NaN"
            else:

View on GitHub (pinned to ccab9f325e)

Solutions

  1. Convert non-string keys to strings before saving: `{str(k): v for k, v in d.items()}`.
  2. Fix metadata construction so ids are stored as string values in a standard field rather than dict keys.
  3. Use a structure like a list of {id, value} pairs or explicit id fields instead of a keyed dict for non-string ids.
  4. Wrap in try/except TypeError on save and transform the offending metadata (the path is given by _crumb_path in the message).

Example fix

// before
metadata = {1: "alpha", "1": "beta"}  # int key would merge with "1"
atomic_save(...)
// after
metadata = {str(k): v for k, v in {1: "alpha", "1": "beta"}.items()}
atomic_save(...)
Defensive patterns

Strategy: validation

Validate before calling

def json_faithful_keys(obj):
    if isinstance(obj, dict):
        assert all(isinstance(k, str) for k in obj), 'non-str JSON key'
        for v in obj.values():
            json_faithful_keys(v)
    elif isinstance(obj, (list, tuple)):
        for v in obj:
            json_faithful_keys(v)
json_faithful_keys(metadata)

Type guard

def str_keys(d: dict) -> bool:
    return all(isinstance(k, str) for k in d)

Try / catch

try:
    turbovec.atomic_save(...)
except TypeError as e:
    if 'side-car key' in str(e):
        metadata = {str(k): v for k, v in walk(metadata)}  # normalize keys
    else:
        raise

Prevention

When it happens

Trigger: Saving a store whose documents/metadata side-car contains a dict with non-string keys, e.g. metadata={1: 'a'} or {(0,1): 'x'}, via atomic_save/load/from_persist_path; cyclic or deeply nested metadata traversed by the checker reaching such a key.

Common situations: Building metadata from parsed config where numeric ids were used as dict keys; merging records from a database whose ids are ints; converting from another store format that allows non-string keys.

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of RyanCodrai/turbovec@ccab9f325e (2026-09-06). Data as JSON: /api/errors/67c744fb173be84b. Report an issue: GitHub.