langflow-ai/langflow · warning · HTTPException
Metadata must be a JSON object.
Error message
Metadata must be a JSON object.
What it means
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).
Source
Thrown at src/backend/base/langflow/api/utils/kb_metadata.py:75
raise HTTPException(status_code=422, detail=msg)
if len(entry) > KB_METADATA_MAX_VALUE_LENGTH:
msg = f"Metadata array entry under '{key}' exceeds {KB_METADATA_MAX_VALUE_LENGTH} characters."
raise HTTPException(status_code=422, detail=msg)
return
msg = f"Metadata value for '{key}' must be a string, number, bool, or string array; got {type(value).__name__}."
raise HTTPException(status_code=422, detail=msg)
def validate_user_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
"""Enforce the user-metadata contract on a decoded dict.
Returns the same dict (a shallow copy is *not* made — callers may mutate
safely once validation passes). Raises :class:`HTTPException` with a 422
status on any violation so FastAPI surfaces an inline error.
"""
if not isinstance(metadata, dict):
msg = "Metadata must be a JSON object."
raise HTTPException(status_code=422, detail=msg)
if len(metadata) > KB_METADATA_MAX_KEYS:
msg = f"Metadata exceeds the {KB_METADATA_MAX_KEYS} key limit."
raise HTTPException(status_code=422, detail=msg)
for key, value in metadata.items():
if not isinstance(key, str) or not _is_valid_key(key):
msg = (
f"Metadata key {key!r} is invalid: must be 1-{KB_METADATA_MAX_KEY_LENGTH} "
"lowercase alphanumeric or underscore characters."
)
raise HTTPException(status_code=422, detail=msg)
if key in KB_METADATA_RESERVED_KEYS:
msg = f"Metadata key '{key}' is reserved for ingestion-internal use."
raise HTTPException(status_code=422, detail=msg)
_validate_value(key, value)
return metadata
def parse_user_metadata(raw: str | None) -> dict[str, Any]:View on GitHub (pinned to 976ec789d2)
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.
Example fix
# before
form.add_field('metadata', json.dumps(tags_list)) # '["a","b"]' -> 422
# after
form.add_field('metadata', json.dumps({'tags': tags_list})) Defensive patterns
Strategy: type-guard
Validate before calling
import json
def to_metadata_field(obj) -> str:
if obj is None or obj == {}:
return '' # empty -> server treats as {}
if not isinstance(obj, dict):
raise TypeError(f'metadata must be a dict, got {type(obj).__name__}')
return json.dumps(obj) Type guard
def is_metadata_object(raw_decoded) -> bool:
return isinstance(raw_decoded, dict) Try / catch
try:
validate_user_metadata(decoded)
except HTTPException as e:
if e.status_code == 422 and e.detail == 'Metadata must be a JSON object.':
decoded = {'value': decoded}; validate_user_metadata(decoded) # or fix shape upstream
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Metadata is not valid JSON: {exc.msg}
- Per-file metadata is not valid JSON: {exc.msg}
- Per-file metadata must be a JSON object keyed by filename.
- Metadata value for '{key}' exceeds {KB_METADATA_MAX_VALUE_LE
- Metadata array '{key}' exceeds {KB_METADATA_MAX_ARRAY_LENGTH
AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14).
Data as JSON: /api/errors/5640b3a29895b0dd.
Report an issue: GitHub.