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

  1. Wrap the payload in an object: send '{"tags": ["a","b"]}' not '["a","b"]'.
  2. If the field may be absent, send an empty object '{}' or omit the form field entirely (parse_user_metadata maps empty/None to {}).
  3. Double-check for double-encoded JSON — decode once client-side before inspecting.
  4. 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

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


AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14). Data as JSON: /api/errors/5640b3a29895b0dd. Report an issue: GitHub.