langflow-ai/langflow · warning · HTTPException

Per-file metadata must be a JSON object keyed by filename.

Error message

Per-file metadata must be a JSON object keyed by filename.

What it means

After successful JSON decoding, parse_per_file_metadata requires the top-level value to be a JSON object keyed by filename ({filename: {...}}). An array, string, number, or null yields 422 'Per-file metadata must be a JSON object keyed by filename.' — this fires only for valid JSON of the wrong shape (invalid JSON fails earlier with error 156).

Source

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


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):
        msg = "Per-file metadata must be a JSON object keyed by filename."
        raise HTTPException(status_code=422, detail=msg)
    if len(decoded) > KB_METADATA_MAX_KEYS:
        msg = f"Per-file metadata exceeds the {KB_METADATA_MAX_KEYS} file limit."
        raise HTTPException(status_code=422, detail=msg)
    out: dict[str, dict[str, Any]] = {}
    for filename, file_metadata in decoded.items():
        if not isinstance(filename, str) or not filename:
            msg = "Per-file metadata keys must be non-empty filename strings."
            raise HTTPException(status_code=422, detail=msg)
        if not isinstance(file_metadata, dict):
            msg = f"Per-file metadata for {filename!r} must be a JSON object."
            raise HTTPException(status_code=422, detail=msg)
        out[filename] = validate_user_metadata(file_metadata)
    return out

View on GitHub (pinned to 976ec789d2)

Solutions

  1. Convert pairs to a dict before serialization: dict(list_of_pairs) then json.dumps.
  2. Validate shape client-side: isinstance(payload, dict) and all(isinstance(v, dict) for v in payload.values()).
  3. Keep filenames as the keys, not duplicated inside the values.
  4. If you only have run-level metadata, use the `metadata` field instead and omit per_file_metadata.

Example fix

# before
per_file = [(name, meta) for name, meta in rows]
form.add_field('per_file_metadata', json.dumps(per_file))  # array -> 422

# after
form.add_field('per_file_metadata', json.dumps(dict(per_file)))
Defensive patterns

Strategy: type-guard

Validate before calling

def to_per_file_map(pairs_or_map) -> dict[str, dict]:
    m = dict(pairs_or_map) if not isinstance(pairs_or_map, dict) else pairs_or_map
    assert all(isinstance(k, str) and k and isinstance(v, dict) for k, v in m.items()), 'need {filename: {...}}'
    return m

Type guard

def is_per_file_object(decoded) -> bool:
    return isinstance(decoded, dict) and all(isinstance(v, dict) for v in decoded.values())

Try / catch

try:
    parse_per_file_metadata(raw)
except HTTPException as e:
    if e.status_code == 422 and 'must be a JSON object keyed by filename' in e.detail:
        decoded = dict(decoded)  # was an array of pairs; re-parse as map
        raw = json.dumps(decoded)
        parse_per_file_metadata(raw)
    else:
        raise

Prevention

When it happens

Trigger: per_file_metadata = '[{"a.pdf": {...}}, {"b.pdf": {...}}]' (array of pairs, common when built from a list) or '"a.pdf:..."' (a bare string). Each inner value is then separately required to be an object (error at 159), but the outer shape check fires here first.

Common situations: Serializing a list of (filename, metadata) tuples with json.dumps (produces an array); a YAML-to-JSON converter emitting a sequence; per-file rows from a dataframe converted as records=[]; clients reusing a generic array payload format.

Related errors


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