MemPalace/mempalace · error · ValueError

metadata is not JSON-serializable: {exc}

Error message

metadata is not JSON-serializable: {exc}

What it means

The metadata dict could not be serialized with json.dumps (TypeError/ValueError), meaning it contains values JSON cannot represent — sets, custom objects, bytes, non-string dict keys, or NaN/Infinity with strict encoders. The original exception text is appended so the offending value is identifiable. This runs after the dict type check.

Source

Thrown at mempalace/logstream.py:165

    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__(
        self,
        db_path: str,
        max_body_bytes: int = DEFAULT_MAX_BODY_BYTES,
        max_artifact_bytes: int = DEFAULT_MAX_ARTIFACT_BYTES,

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Convert non-native values before the call: datetime.isoformat(), list(set), bytes.decode(), str(Path).
  2. Use a default hook by pre-serializing: json.dumps(metadata, default=str) then json.loads the result before passing.
  3. Replace unserializable entries with plain str/int/float/bool/None/list/dict.

Example fix

// before
ls.append_event(..., metadata={"at": datetime.now(), "tags": {"a"}})
// after
ls.append_event(..., metadata={"at": datetime.now(timezone.utc).isoformat(), "tags": ["a"]})
Defensive patterns

Strategy: validation

Validate before calling

def json_safe_metadata(md: dict) -> dict:
    return json.loads(json.dumps(md, ensure_ascii=False, default=str))

metadata = json_safe_metadata(metadata)  # datetime/set/Path become JSON-native

Type guard

def is_json_serializable(md) -> bool:
    if not isinstance(md, dict):
        return False
    try:
        json.dumps(md, ensure_ascii=False)
        return True
    except (TypeError, ValueError):
        return False

Try / catch

try:
    evt = ls.append_event(..., metadata=metadata)
except ValueError as e:
    if "not JSON-serializable" in str(e):
        evt = ls.append_event(..., metadata=json.loads(json.dumps(metadata, default=str)))
    else:
        raise

Prevention

When it happens

Trigger: append_event(metadata={'ids': {1, 2}}) (set); metadata={'when': datetime.now()} (datetime); metadata={'blob': b'x'} (bytes); metadata={(1,2): 'v'} (tuple key); nested dataclass instances.

Common situations: Reusing ORM/model objects as metadata; mixing stdlib types (Path, datetime, set) that are convenient in Python but not JSON-native; cache values from third-party libs leaking into annotations.

Related errors


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