langflow-ai/langflow · warning · HTTPException

Metadata is not valid JSON: {exc.msg}

Error message

Metadata is not valid JSON: {exc.msg}

What it means

parse_user_metadata json.loads()es the raw multipart `metadata` form field; if the string is not valid JSON (syntax error), it raises 422 with 'Metadata is not valid JSON: {parser message}' chaining the JSONDecodeError. Empty or missing field is fine (returns {}); this error means a non-empty, syntactically broken payload.

Source

Thrown at src/backend/base/langflow/api/utils/kb_metadata.py:101

                "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]:
    """Decode + validate the ``metadata`` form field. Empty/None → ``{}``."""
    if not raw:
        return {}
    try:
        decoded = json.loads(raw)
    except json.JSONDecodeError as exc:
        msg = f"Metadata is not valid JSON: {exc.msg}"
        raise HTTPException(status_code=422, detail=msg) from exc
    return validate_user_metadata(decoded)


def parse_per_file_metadata(raw: str | None) -> dict[str, dict[str, Any]]:
    """Decode + validate the ``per_file_metadata`` form field.

    Shape: ``{filename: {metadata_dict}, ...}``. Each inner dict goes through
    the same validator as run-level metadata, so per-file overrides obey the
    same key/value rules. Empty/None → ``{}``.
    """
    if not raw:
        return {}
    try:
        decoded = json.loads(raw)
    except json.JSONDecodeError as exc:
        msg = f"Per-file metadata is not valid JSON: {exc.msg}"
        raise HTTPException(status_code=422, detail=msg) from exc
    if not isinstance(decoded, dict):

View on GitHub (pinned to 976ec789d2)

Solutions

  1. Always build the field with json.dumps(obj) — never f-strings or str(dict) (single quotes are not JSON).
  2. Read the parser message in the 422 detail: it states exactly what was expected at the failing offset.
  3. If the field may be empty, omit it or send '' (both map to {}), rather than 'null' text handled elsewhere.
  4. Test round-trip locally: json.loads(json.dumps(meta)) before sending.

Example fix

# before
form.add_field('metadata', str({'a': 1}))  # "{'a': 1}" -> 422

# after
import json
form.add_field('metadata', json.dumps({'a': 1}))
Defensive patterns

Strategy: validation

Validate before calling

import json

def safe_metadata_field(obj) -> str:
    return json.dumps(obj) if obj else ''  # valid JSON or empty

Type guard

def is_valid_metadata_json(raw: str) -> bool:
    if not raw:
        return True
    try:
        json.loads(raw); return True
    except json.JSONDecodeError:
        return False

Try / catch

try:
    parse_user_metadata(raw)
except HTTPException as e:
    if e.status_code == 422 and 'not valid JSON' in e.detail:
        raise ValueError(f'fix JSON syntax: {e.detail}') from e
    raise

Prevention

When it happens

Trigger: metadata form field set to 'project=alpha' (query-string style), '{project: "alpha"}' (unquoted key), a trailing comma, or truncated JSON from string slicing. json.loads' own message ('Expecting property name enclosed in double quotes', 'Unterminated string starting at...') is embedded, pinpointing the syntax position.

Common situations: Hand-building the field with f-strings or manual concatenation instead of json.dumps; curl --form with missing quotes; content mutated by a gateway or form encoder (unescaped quotes); copied payloads that were pretty-printed then re-wrapped in extra quotes once too many times.

Related errors


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