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
- Convert non-string keys to strings before saving: `{str(k): v for k, v in d.items()}`.
- Fix metadata construction so ids are stored as string values in a standard field rather than dict keys.
- Use a structure like a list of {id, value} pairs or explicit id fields instead of a keyed dict for non-string ids.
- 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
- Normalize all metadata dict keys with str(k) at construction time.
- Store ids as values in fields, not as dict keys, when they are not strings.
- Avoid tuples/None as dict keys in data destined for JSON.
- Round-trip test: json.loads(json.dumps(payload)) in CI for representative stores.
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
- side-car value at {_crumb_path(entry)} is {obj!r}, which JSO
- persisted store is corrupt: {len(missing)} {what} id(s) pres
- module {__name__!r} has no attribute {name!r}
- duplicate id in batch: {k!r}
- {prefix} {version}; this turbovec accepts versions {list(com
AI-assisted analysis of RyanCodrai/turbovec@ccab9f325e (2026-09-06).
Data as JSON: /api/errors/67c744fb173be84b.
Report an issue: GitHub.