MemPalace/mempalace · error · ValueError

metadata must be an object

Error message

metadata must be an object

What it means

The `metadata` argument to append_event or put_artifact must be a Python dict (or None, which becomes '{}') because it is serialized to canonical sorted-key JSON and stored in a text column. Lists, strings, and scalars are rejected before serialization. The metadata is meant to be a flat structured annotation object.

Source

Thrown at mempalace/logstream.py:161

    if value is None:
        return ""
    if not isinstance(value, str):
        raise ValueError(f"{field_name} must be a string")
    if "\x00" in value:
        raise ValueError(f"{field_name} contains null bytes")
    value = strip_lone_surrogates(value)
    size = len(value.encode("utf-8"))
    if size > max_bytes:
        raise ValueError(f"{field_name} is {size} bytes; maximum is {max_bytes} bytes")
    return value


def _sanitize_metadata(value) -> str:
    """Validate optional metadata dict and return its canonical JSON text."""
    if value is None:
        return "{}"
    if not isinstance(value, dict):
        raise ValueError("metadata must be an object")
    try:
        encoded = json.dumps(value, ensure_ascii=False, sort_keys=True)
    except (TypeError, ValueError) as exc:
        raise ValueError(f"metadata is not JSON-serializable: {exc}") from None
    if len(encoded.encode("utf-8")) > MAX_METADATA_BYTES:
        raise ValueError(f"metadata exceeds maximum size of {MAX_METADATA_BYTES} bytes")
    return encoded


class Logstream:
    """Durable append-only coordination log (events + artifacts).

    Storage and threading mirror ``KnowledgeGraph``: one SQLite file in
    WAL mode, a per-instance lock around writes, ``check_same_thread=False``
    so the MCP HTTP server can call from worker threads.
    """

    def __init__(

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Pass a plain dict: metadata={'priority': 1, 'source': 'hub'}.
  2. If you received JSON text, parse it first: metadata=json.loads(raw) and confirm it is a dict.
  3. Use metadata=None (or omit) when there is nothing to attach.

Example fix

// before
evt = ls.append_event(..., metadata="{\"priority\": 1}")
// after
import json
evt = ls.append_event(..., metadata=json.loads("{\"priority\": 1}"))
Defensive patterns

Strategy: type-guard

Validate before calling

def as_metadata(value):
    if value is None:
        return None
    if isinstance(value, str):
        value = json.loads(value)  # accept pre-encoded JSON
    if not isinstance(value, dict):
        raise TypeError("metadata must be a dict")
    return value

metadata = as_metadata(raw_metadata)

Type guard

def is_valid_metadata(m) -> bool:
    return m is None or isinstance(m, dict)

Try / catch

try:
    evt = ls.append_event(..., metadata=metadata)
except ValueError as e:
    if "must be an object" in str(e):
        evt = ls.append_event(..., metadata={"value": metadata})  # wrap scalars
    else:
        raise

Prevention

When it happens

Trigger: append_event(metadata='[1,2]') (string); metadata=[('k','v')] (list of tuples); metadata='ok'; put_artifact(metadata=json_string) — passing pre-encoded JSON text instead of the dict itself.

Common situations: Callers double-serializing (json.dumps then passing the string); handlers forwarding a JSON array from an MCP request; teams migrating from a schema where metadata was a string blob.

Related errors


AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15). Data as JSON: /api/errors/ce4eb1664d0f2b29. Report an issue: GitHub.