{"record":{"id":"5f563e8419a2881b","repo":"langflow-ai/langflow","slug":"per-file-metadata-must-be-a-json-object-keyed-by-f","errorCode":null,"errorMessage":"Per-file metadata must be a JSON object keyed by filename.","messagePattern":"Per-file metadata must be a JSON object keyed by filename\\.","errorType":"validation","errorClass":"HTTPException","httpStatus":422,"severity":"warning","filePath":"src/backend/base/langflow/api/utils/kb_metadata.py","lineNumber":121,"sourceCode":"\n\ndef parse_per_file_metadata(raw: str | None) -> dict[str, dict[str, Any]]:\n    \"\"\"Decode + validate the ``per_file_metadata`` form field.\n\n    Shape: ``{filename: {metadata_dict}, ...}``. Each inner dict goes through\n    the same validator as run-level metadata, so per-file overrides obey the\n    same key/value rules. Empty/None → ``{}``.\n    \"\"\"\n    if not raw:\n        return {}\n    try:\n        decoded = json.loads(raw)\n    except json.JSONDecodeError as exc:\n        msg = f\"Per-file metadata is not valid JSON: {exc.msg}\"\n        raise HTTPException(status_code=422, detail=msg) from exc\n    if not isinstance(decoded, dict):\n        msg = \"Per-file metadata must be a JSON object keyed by filename.\"\n        raise HTTPException(status_code=422, detail=msg)\n    if len(decoded) > KB_METADATA_MAX_KEYS:\n        msg = f\"Per-file metadata exceeds the {KB_METADATA_MAX_KEYS} file limit.\"\n        raise HTTPException(status_code=422, detail=msg)\n    out: dict[str, dict[str, Any]] = {}\n    for filename, file_metadata in decoded.items():\n        if not isinstance(filename, str) or not filename:\n            msg = \"Per-file metadata keys must be non-empty filename strings.\"\n            raise HTTPException(status_code=422, detail=msg)\n        if not isinstance(file_metadata, dict):\n            msg = f\"Per-file metadata for {filename!r} must be a JSON object.\"\n            raise HTTPException(status_code=422, detail=msg)\n        out[filename] = validate_user_metadata(file_metadata)\n    return out\n","sourceCodeStart":103,"sourceCodeEnd":135,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/utils/kb_metadata.py#L103-L135","documentation":"After successful JSON decoding, parse_per_file_metadata requires the top-level value to be a JSON object keyed by filename ({filename: {...}}). An array, string, number, or null yields 422 'Per-file metadata must be a JSON object keyed by filename.' — this fires only for valid JSON of the wrong shape (invalid JSON fails earlier with error 156).","triggerScenarios":"per_file_metadata = '[{\"a.pdf\": {...}}, {\"b.pdf\": {...}}]' (array of pairs, common when built from a list) or '\"a.pdf:...\"' (a bare string). Each inner value is then separately required to be an object (error at 159), but the outer shape check fires here first.","commonSituations":"Serializing a list of (filename, metadata) tuples with json.dumps (produces an array); a YAML-to-JSON converter emitting a sequence; per-file rows from a dataframe converted as records=[]; clients reusing a generic array payload format.","solutions":["Convert pairs to a dict before serialization: dict(list_of_pairs) then json.dumps.","Validate shape client-side: isinstance(payload, dict) and all(isinstance(v, dict) for v in payload.values()).","Keep filenames as the keys, not duplicated inside the values.","If you only have run-level metadata, use the `metadata` field instead and omit per_file_metadata."],"exampleFix":"# before\nper_file = [(name, meta) for name, meta in rows]\nform.add_field('per_file_metadata', json.dumps(per_file))  # array -> 422\n\n# after\nform.add_field('per_file_metadata', json.dumps(dict(per_file)))","handlingStrategy":"type-guard","validationCode":"def to_per_file_map(pairs_or_map) -> dict[str, dict]:\n    m = dict(pairs_or_map) if not isinstance(pairs_or_map, dict) else pairs_or_map\n    assert all(isinstance(k, str) and k and isinstance(v, dict) for k, v in m.items()), 'need {filename: {...}}'\n    return m","typeGuard":"def is_per_file_object(decoded) -> bool:\n    return isinstance(decoded, dict) and all(isinstance(v, dict) for v in decoded.values())","tryCatchPattern":"try:\n    parse_per_file_metadata(raw)\nexcept HTTPException as e:\n    if e.status_code == 422 and 'must be a JSON object keyed by filename' in e.detail:\n        decoded = dict(decoded)  # was an array of pairs; re-parse as map\n        raw = json.dumps(decoded)\n        parse_per_file_metadata(raw)\n    else:\n        raise","preventionTips":["Serialize a dict, not a list of tuples/records.","Client-side assert isinstance(payload, dict).","Filenames are keys only — don't also nest them inside values."],"tags":["knowledge-base","metadata","validation","json","http-422"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}