{"record":{"id":"5640b3a29895b0dd","repo":"langflow-ai/langflow","slug":"metadata-must-be-a-json-object","errorCode":null,"errorMessage":"Metadata must be a JSON object.","messagePattern":"Metadata must be a JSON object\\.","errorType":"validation","errorClass":"HTTPException","httpStatus":422,"severity":"warning","filePath":"src/backend/base/langflow/api/utils/kb_metadata.py","lineNumber":75,"sourceCode":"                raise HTTPException(status_code=422, detail=msg)\n            if len(entry) > KB_METADATA_MAX_VALUE_LENGTH:\n                msg = f\"Metadata array entry under '{key}' exceeds {KB_METADATA_MAX_VALUE_LENGTH} characters.\"\n                raise HTTPException(status_code=422, detail=msg)\n        return\n    msg = f\"Metadata value for '{key}' must be a string, number, bool, or string array; got {type(value).__name__}.\"\n    raise HTTPException(status_code=422, detail=msg)\n\n\ndef validate_user_metadata(metadata: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Enforce the user-metadata contract on a decoded dict.\n\n    Returns the same dict (a shallow copy is *not* made — callers may mutate\n    safely once validation passes). Raises :class:`HTTPException` with a 422\n    status on any violation so FastAPI surfaces an inline error.\n    \"\"\"\n    if not isinstance(metadata, dict):\n        msg = \"Metadata must be a JSON object.\"\n        raise HTTPException(status_code=422, detail=msg)\n    if len(metadata) > KB_METADATA_MAX_KEYS:\n        msg = f\"Metadata exceeds the {KB_METADATA_MAX_KEYS} key limit.\"\n        raise HTTPException(status_code=422, detail=msg)\n    for key, value in metadata.items():\n        if not isinstance(key, str) or not _is_valid_key(key):\n            msg = (\n                f\"Metadata key {key!r} is invalid: must be 1-{KB_METADATA_MAX_KEY_LENGTH} \"\n                \"lowercase alphanumeric or underscore characters.\"\n            )\n            raise HTTPException(status_code=422, detail=msg)\n        if key in KB_METADATA_RESERVED_KEYS:\n            msg = f\"Metadata key '{key}' is reserved for ingestion-internal use.\"\n            raise HTTPException(status_code=422, detail=msg)\n        _validate_value(key, value)\n    return metadata\n\n\ndef parse_user_metadata(raw: str | None) -> dict[str, Any]:","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/utils/kb_metadata.py#L57-L93","documentation":"validate_user_metadata starts by asserting the decoded payload is a Python dict (a JSON object). Anything else — an array, string, number, or null — is rejected with 422 'Metadata must be a JSON object.' This runs before key/value rules; it is the outermost shape check for both run-level metadata and each per-file inner dict (which funnels through the same validator).","triggerScenarios":"A metadata form field containing '[1,2,3]', '\"just a string\"', '42', or 'null' instead of '{...}'. For per_file_metadata this same error appears if any inner value (the per-file dict) is not an object, raised from validate_user_metadata called on it.","commonSituations":"Sending a JSON-encoded list of tag strings directly instead of wrapping them in an object; upstream code that json.dumps a list variable by mistake; copy-paste errors where a whole metadata JSON document is double-encoded (a string after one decode pass); null from defaulting an optional field to None and serializing it.","solutions":["Wrap the payload in an object: send '{\"tags\": [\"a\",\"b\"]}' not '[\"a\",\"b\"]'.","If the field may be absent, send an empty object '{}' or omit the form field entirely (parse_user_metadata maps empty/None to {}).","Double-check for double-encoded JSON — decode once client-side before inspecting.","Add a client-side isinstance(payload, dict) check before submitting."],"exampleFix":"# before\nform.add_field('metadata', json.dumps(tags_list))  # '[\"a\",\"b\"]' -> 422\n\n# after\nform.add_field('metadata', json.dumps({'tags': tags_list}))","handlingStrategy":"type-guard","validationCode":"import json\n\ndef to_metadata_field(obj) -> str:\n    if obj is None or obj == {}:\n        return ''  # empty -> server treats as {}\n    if not isinstance(obj, dict):\n        raise TypeError(f'metadata must be a dict, got {type(obj).__name__}')\n    return json.dumps(obj)","typeGuard":"def is_metadata_object(raw_decoded) -> bool:\n    return isinstance(raw_decoded, dict)","tryCatchPattern":"try:\n    validate_user_metadata(decoded)\nexcept HTTPException as e:\n    if e.status_code == 422 and e.detail == 'Metadata must be a JSON object.':\n        decoded = {'value': decoded}; validate_user_metadata(decoded)  # or fix shape upstream\n    else:\n        raise","preventionTips":["Always json.dumps a dict; never a list or scalar.","Omit the form field instead of sending 'null'.","Guard with isinstance(obj, dict) before building the multipart part."],"tags":["knowledge-base","metadata","validation","json","http-422"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}