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
- Pass a plain dict: metadata={'priority': 1, 'source': 'hub'}.
- If you received JSON text, parse it first: metadata=json.loads(raw) and confirm it is a dict.
- 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
- Pass dicts straight through; never json.dumps before the call — the library serializes canonically itself.
- json.loads any inbound JSON text at the system boundary and assert the result is a dict.
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
- {field_name} must be a string
- metadata is not JSON-serializable: {exc}
- metadata exceeds maximum size of {MAX_METADATA_BYTES} bytes
- artifact_ids must be a list of artifact id strings
- metadata key {key!r} clashes with a reserved Milvus field
AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15).
Data as JSON: /api/errors/ce4eb1664d0f2b29.
Report an issue: GitHub.