{"record":{"id":"f5b5c024a53e3602","repo":"jamiepine/voicebox","slug":"invalid-samples-json-must-be-a-dictionary","errorCode":null,"errorMessage":"Invalid samples.json: must be a dictionary","messagePattern":"Invalid samples\\.json: must be a dictionary","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/services/export_import.py","lineNumber":165,"sourceCode":"            if \"samples.json\" not in namelist:\n                raise ValueError(\"ZIP archive missing samples.json\")\n            \n            # Read manifest\n            manifest_data = json.loads(zip_file.read(\"manifest.json\"))\n            \n            if \"version\" not in manifest_data:\n                raise ValueError(\"Invalid manifest.json: missing version\")\n            \n            if \"profile\" not in manifest_data:\n                raise ValueError(\"Invalid manifest.json: missing profile\")\n            \n            profile_data = manifest_data[\"profile\"]\n            \n            # Read samples mapping\n            samples_data = json.loads(zip_file.read(\"samples.json\"))\n            \n            if not isinstance(samples_data, dict):\n                raise ValueError(\"Invalid samples.json: must be a dictionary\")\n            \n            # Get unique profile name\n            original_name = profile_data.get(\"name\", \"Imported Profile\")\n            unique_name = _get_unique_profile_name(original_name, db)\n            \n            # Create profile\n            profile_create = VoiceProfileCreate(\n                name=unique_name,\n                description=profile_data.get(\"description\"),\n                language=profile_data.get(\"language\", \"en\"),\n            )\n            \n            profile = await create_profile(profile_create, db)\n\n            # Extract and add samples\n            profile_dir = config.get_profiles_dir() / profile.id\n            profile_dir.mkdir(parents=True, exist_ok=True)\n","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/services/export_import.py#L147-L183","documentation":"samples.json must JSON-parse to a dict (filename -> reference_text). A list, scalar, or any other top-level JSON type is rejected because the importer iterates samples_data.items() expecting string keys. Raised after JSON parsing succeeds.","triggerScenarios":"samples.json contains a JSON array of objects, a bare string/number, or any non-object type — typically produced by a non-conforming migration tool or hand-edit.","commonSituations":"Third-party migration script emitting an array; manual edit turning the object into a list of records.","solutions":["Re-export with the official exporter.","If rebuilding, make samples.json a flat object: {\"<filename>.wav\": \"<reference text>\"}."],"exampleFix":"// before\n[{\"file\": \"a.wav\", \"text\": \"hi\"}, {\"file\": \"b.wav\", \"text\": \"bye\"}]\n\n// after\n{\"a.wav\": \"hi\", \"b.wav\": \"bye\"}","handlingStrategy":"validation","validationCode":"import io, json, zipfile\n\ndef samples_is_dict(file_bytes) -> bool:\n    with zipfile.ZipFile(io.BytesIO(file_bytes)) as z:\n        return isinstance(json.loads(z.read(\"samples.json\")), dict)","typeGuard":"def is_samples_mapping(v) -> bool:\n    return isinstance(v, dict) and all(isinstance(k, str) for k in v)","tryCatchPattern":"try:\n    await import_profile_from_zip(file_bytes, db)\nexcept ValueError as e:\n    if \"must be a dictionary\" in str(e):\n        # reformat samples.json into a flat {filename: text} mapping\n        ...\n    raise","preventionTips":["Validate samples.json top-level type before importing.","Use the official exporter; don't hand-craft the mapping."],"tags":["profiles","import","samples","validation","schema"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}