{"record":{"id":"90948833daf86387","repo":"RyanCodrai/turbovec","slug":"side-car-value-at-crumb-path-entry-is-obj-r","errorCode":null,"errorMessage":"side-car value at {_crumb_path(entry)} is {obj!r}, which JSON cannot represent: it would be written as a bare {token} token that RFC 8259 forbids. Other JSON readers reject the file (serde_json, JSON.parse) or silently rewrite the value to null (jq). Replace it with None or a finite number before saving.","messagePattern":"side-car value at (.+?) is (.+?), which JSON cannot represent: it would be written as a bare (.+?) token that RFC 8259 forbids\\. Other JSON readers reject the file \\(serde_json, JSON\\.parse\\) or silently rewrite the value to null \\(jq\\)\\. Replace it with None or a finite number before saving\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"turbovec-python/python/turbovec/_persist.py","lineNumber":329,"sourceCode":"                        f\"{_crumb_path(entry)} is {type(key).__name__}, not \"\n                        f\"str. JSON object keys are strings, so writing it \"\n                        f\"would stringify the key and silently merge it with \"\n                        f\"any existing {str(key)!r} key, losing data on \"\n                        f\"reload. Convert the key to a str before saving.\"\n                    )\n                stack.append((value, entry, key))\n        elif isinstance(obj, (list, tuple)):\n            if id(obj) in seen:\n                continue\n            seen.add(id(obj))\n            for i, value in enumerate(obj):\n                stack.append((value, entry, i))\n        elif isinstance(obj, float) and not math.isfinite(obj):\n            if math.isnan(obj):\n                token = \"NaN\"\n            else:\n                token = \"Infinity\" if obj > 0 else \"-Infinity\"\n            raise ValueError(\n                f\"side-car value at {_crumb_path(entry)} is {obj!r}, \"\n                f\"which JSON cannot represent: it would be written as a bare \"\n                f\"{token} token that RFC 8259 forbids. Other JSON readers \"\n                f\"reject the file (serde_json, JSON.parse) or silently \"\n                f\"rewrite the value to null (jq). Replace it with None or a \"\n                f\"finite number before saving.\"\n            )\n\n\ndef atomic_save(index, index_path, payload: Any, sidecar_path) -> None:\n    \"\"\"Atomically persist an index + JSON side-car pair — the shared\n    write path for all four integrations' save methods.\n\n    The failure-ordering guarantees:\n\n    1. ``payload`` is validated and JSON-serialized fully in memory\n       *first*, so a value whose JSON form would lose data or not be\n       portable JSON raises before any file is touched: a non-serializable","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/RyanCodrai/turbovec/blob/ccab9f325e6ce2a270a87daf01ae4e443bcf2d49/turbovec-python/python/turbovec/_persist.py#L311-L347","documentation":"_check_json_faithful also guards values: JSON (RFC 8259) has no representation for NaN or Infinity, so json.dump would emit bare NaN/Infinity tokens that other parsers reject or rewrite to null. To guarantee the side-car is faithfully reloadable, the check raises ValueError naming the value and its path before the file is written.","triggerScenarios":"Saving (atomic_save/load/from_persist_path) a store whose metadata side-car contains float('nan'), float('inf'), or float('-inf'), typically from a distance/score computation or missing-value sentinel; the check recurses through nested metadata.","commonSituations":"Storing raw similarity scores that came out NaN/inf; using float('inf') as a default or sentinel in metadata; numpy float32/float64 values converted to Python floats with NaN from division by zero.","solutions":["Sanitize metadata before saving: replace non-finite floats with None or a finite clamped value (e.g. math.isfinite check).","Fix the source computation producing NaN/inf (guard division by zero, masked arrays, np.nan_to_num on numpy data).","Store such values as strings or a sentinel object documented as non-numeric if the semantics matter.","Wrap save in try/except ValueError and clean the reported path (given by _crumb_path) before retrying."],"exampleFix":"// before\nmeta = {\"score\": float(\"inf\")}\natomic_save(...)\n// after\nimport math\nmeta = {\"score\": v if math.isfinite(v := float(\"inf\")) else None}  # or np.nan_to_num(scores)\natomic_save(...)","handlingStrategy":"validation","validationCode":"import math\ndef no_nonfinite(obj):\n    if isinstance(obj, float):\n        assert math.isfinite(obj), f'non-finite value: {obj}'\n    elif isinstance(obj, dict):\n        for v in obj.values(): no_nonfinite(v)\n    elif isinstance(obj, (list, tuple)):\n        for v in obj: no_nonfinite(v)\nno_nonfinite(metadata)","typeGuard":"def json_safe_floats(obj) -> bool:\n    import math\n    if isinstance(obj, float):\n        return math.isfinite(obj)\n    if isinstance(obj, dict):\n        return all(json_safe_floats(v) for v in obj.values())\n    if isinstance(obj, (list, tuple)):\n        return all(json_safe_floats(v) for v in obj)\n    return True","tryCatchPattern":"try:\n    turbovec.atomic_save(...)\nexcept ValueError as e:\n    if 'NaN' in str(e) or 'Infinity' in str(e):\n        metadata = sanitize_nonfinite(metadata)  # np.nan_to_num / None\n    else:\n        raise","preventionTips":["Run np.nan_to_num or an isfinite clamp on scores before attaching them to metadata.","Avoid float('inf')/-inf as sentinels in persisted metadata; use None.","Validate metadata with a json round-trip (parse_non_standard JSON off) in tests.","Guard arithmetic that can produce NaN (0/0, inf-inf) at the source."],"tags":["python","json","serialization","nan","infinity"],"backgroundTag":"json-serialization-failed","analyzedSha":"ccab9f325e6ce2a270a87daf01ae4e443bcf2d49","analyzedAt":"2026-09-06T08:39:18.516Z","contentChangedAt":"2026-09-06T08:39:18.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}