{"record":{"id":"2d93690f5a75f838","repo":"langflow-ai/langflow","slug":"per-file-metadata-is-not-valid-json-exc-msg","errorCode":null,"errorMessage":"Per-file metadata is not valid JSON: {exc.msg}","messagePattern":"Per-file metadata is not valid JSON: (.+?)","errorType":"validation","errorClass":"HTTPException","httpStatus":422,"severity":"warning","filePath":"src/backend/base/langflow/api/utils/kb_metadata.py","lineNumber":118,"sourceCode":"        msg = f\"Metadata is not valid JSON: {exc.msg}\"\n        raise HTTPException(status_code=422, detail=msg) from exc\n    return validate_user_metadata(decoded)\n\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":100,"sourceCodeEnd":135,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/utils/kb_metadata.py#L100-L135","documentation":"parse_per_file_metadata json.loads()es the `per_file_metadata` form field; a syntax error in the JSON raises 422 'Per-file metadata is not valid JSON: {parser message}' with the JSONDecodeError chained. This is distinct from error 155 only in field name — the expected shape is {filename: {metadata object}, ...}.","triggerScenarios":"per_file_metadata set to a malformed string — unquoted keys, single quotes, trailing commas, or a JSON array of objects instead of an object keyed by filename (the array case parses fine and then fails error 157). Embedded parser message (e.g. 'Expecting value: line 1 column 1') identifies the offset.","commonSituations":"Building the per-file map by string concatenation ('{' + ','.join(parts) + '}'); double-encoding (a string that is itself a JSON string); clients that serialize {filename: dict} with a non-JSON serializer (Python repr); passing the whole request body as the field by mistake.","solutions":["Construct with json.dumps({fname: meta_dict, ...}).","Ensure values are dicts BEFORE dumps — json.dumps cannot express a Python dict key that is fine but callers sometimes pass lists of (name, meta) tuples; convert those to a dict first.","Check the embedded parser message for the exact offset and fix that spot.","Omit the field entirely when there are no per-file overrides (empty/None -> {})."],"exampleFix":"# before\nform.add_field('per_file_metadata', str(per_file))  # repr -> 422\n\n# after\nform.add_field('per_file_metadata', json.dumps(per_file))  # e.g. {\"a.pdf\": {\"dept\": \"fin\"}}","handlingStrategy":"validation","validationCode":"import json\n\ndef safe_per_file_field(per_file: dict | None) -> str:\n    if not per_file:\n        return ''\n    assert all(isinstance(k, str) and k and isinstance(v, dict) for k, v in per_file.items())\n    return json.dumps(per_file)","typeGuard":"def is_valid_per_file_json(raw: str) -> bool:\n    if not raw:\n        return True\n    try:\n        d = json.loads(raw)\n    except json.JSONDecodeError:\n        return False\n    return isinstance(d, dict)","tryCatchPattern":"try:\n    parse_per_file_metadata(raw)\nexcept HTTPException as e:\n    if e.status_code == 422 and 'Per-file metadata is not valid JSON' in e.detail:\n        raise ValueError(f'rebuild field with json.dumps: {e.detail}') from e\n    raise","preventionTips":["json.dumps the {filename: {...}} dict; never repr() or manual strings.","Omit the field when no per-file overrides exist.","Convert list-of-pairs to dict() before serializing."],"tags":["knowledge-base","metadata","json","validation","http-422"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}