666ghj/MiroFish · error · StarHistoryError

history state must be a JSON object

Error message

history state must be a JSON object

What it means

validate_state requires the loaded history state to be a JSON object at the top level; anything else (list, string, number, null, bool) raises StarHistoryError('history state must be a JSON object'). This fires before _expect_keys so callers get a clear message rather than a KeyError on dict access.

Source

Thrown at scripts/star_history.py:346

    normalized = _normalize_now(value)
    return normalized.strftime("%Y-%m-%dT%H:%M:%SZ")


def _normalize_now(value: datetime) -> datetime:
    if value.tzinfo is None or value.utcoffset() != timedelta(0):
        raise StarHistoryError("clock must return a UTC datetime")
    return value.astimezone(UTC).replace(microsecond=0)


def _expect_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None:
    actual = set(value)
    if actual != expected:
        raise StarHistoryError(f"{label} contains missing or unknown fields")


def validate_state(state: Any) -> None:
    if not isinstance(state, dict):
        raise StarHistoryError("history state must be a JSON object")
    _expect_keys(
        state,
        {
            "schema_version",
            "repository",
            "timezone",
            "ongoing_interval_days",
            "reconstruction",
            "snapshots",
        },
        "history state",
    )
    if state["schema_version"] != 1 or type(state["schema_version"]) is not int:
        raise StarHistoryError("unsupported history schema_version")
    if state["repository"] != REPOSITORY:
        raise StarHistoryError("history repository does not match configured repository")
    if state["timezone"] != "UTC":
        raise StarHistoryError("history timezone must be UTC")

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Inspect the first bytes of the state file to see what shape it actually is
  2. Regenerate the state file via the script's normal run path (full reconstruction from GitHub)
  3. If migrating from an old array format, write a one-off migration or discard and rebuild

Example fix

// before (state file)
[ {"date": "2024-01-01", "stars": 10} ]

// after (state file)
{ "schema_version": 1, "repository": "666ghj/MiroFish", "timezone": "UTC", "ongoing_interval_days": 13, "reconstruction": {"method": "...", "generated_at": "2024-01-15T10:30:00Z", "daily": []}, "snapshots": [] }
Defensive patterns

Strategy: type-guard

Validate before calling

import json
with state_path.open() as fh:
    state = json.load(fh)
if not isinstance(state, dict):
    raise StarHistoryError(f"state file is a {type(state).__name__}, expected a JSON object")

Type guard

def is_state_object(value: object) -> TypeGuard[dict]:
    return isinstance(value, dict)

Try / catch

try:
    validate_state(state)
except StarHistoryError as exc:
    if "must be a JSON object" in str(exc):
        regenerate_state_file()  # full reconstruction from GitHub
    else:
        raise

Prevention

When it happens

Trigger: A state file containing '[...]' (a JSON array, e.g. an old snapshots-only format), a bare string, or 'null'; also a truncated file that happens to decode to a scalar.

Common situations: State format changed across script versions (array -> object); the file was overwritten by another tool's output; a failed atomic write left partial JSON.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/8409a264b976e2f3. Report an issue: GitHub.