opendatalab/MinerU · error · ValueError

{payload_name} must be a JSON object

Error message

{payload_name} must be a JSON object

What it means

ValueError('{payload_name} must be a JSON object') raised by _parse_json_object_response in router.py when the worker's response parsed as valid JSON but the top-level value is not a dict (e.g. a JSON list, string, or number). The router expects every worker payload (health stats, task status) to be a JSON object, so a syntactically fine but wrong-shaped reply is rejected. Usually indicates a version mismatch between router and worker, or a different service answering on that URL.

Source

Thrown at mineru/cli/router.py:111

    try:
        value = int(os.getenv(name, str(default)))
    except ValueError:
        return default
    if value < minimum:
        return default
    return value


def _parse_json_object_response(
    response: httpx.Response,
    payload_name: str,
) -> dict[str, Any]:
    try:
        payload = response.json()
    except ValueError as exc:
        raise ValueError(f"{payload_name} is not valid JSON") from exc
    if not isinstance(payload, dict):
        raise ValueError(f"{payload_name} must be a JSON object")
    return payload


def get_task_retention_seconds() -> int:
    return get_int_env(
        "MINERU_API_TASK_RETENTION_SECONDS",
        DEFAULT_TASK_RETENTION_SECONDS,
        minimum=0,
    )


def get_task_cleanup_interval_seconds() -> int:
    return get_int_env(
        "MINERU_API_TASK_CLEANUP_INTERVAL_SECONDS",
        DEFAULT_TASK_CLEANUP_INTERVAL_SECONDS,
        minimum=1,
    )

View on GitHub (pinned to 4fe4bde114)

Solutions

  1. Log/inspect response.json() to see the actual top-level type and content.
  2. Confirm the URL points at a real mineru worker of the matching version (check its /health payload shape).
  3. Align router and worker versions (protocol_version is also verified elsewhere; mismatches surface quickly).
  4. Fix test mocks to return objects like {'status': 'healthy', ...}.

Example fix

# before (mock worker)
@pytest.fixture
def health():
    return [200, {'status': 'healthy'}]  # top-level list -> ValueError

# after
@pytest.fixture
def health():
    return {'status_code': 200, 'json': {'status': 'healthy', 'protocol_version': API_PROTOCOL_VERSION}}
Defensive patterns

Strategy: type-guard

Validate before calling

import json

def is_json_object_response(response) -> bool:
    try:
        return isinstance(response.json(), dict)
    except ValueError:
        return False

Type guard

def is_worker_payload(payload) -> bool:
    return isinstance(payload, dict) and 'status' in payload

Try / catch

try:
    payload = _parse_json_object_response(response, name)
except ValueError as exc:
    if 'must be a JSON object' in str(exc):
        logger.error('worker replied with top-level %s', type(response.json()).__name__)
    raise

Prevention

When it happens

Trigger: The worker endpoint returns a JSON array (older/newer protocol); a generic API (not a mineru worker) answering on the configured port and returning scalar/array JSON; middleware wrapping responses in a list; a hand-rolled mock worker used in tests that returns a bare list.

Common situations: Mixing mineru versions where the response schema changed; SERVER_URL pointing at the wrong service; test mocks not shaped like the real protocol.

Related errors


AI-assisted analysis of opendatalab/MinerU@4fe4bde114 (2026-08-14). Data as JSON: /api/errors/ddc6c8c78f3e3f14. Report an issue: GitHub.