affaan-m/ECC · error · ValueError

Expected a JSON object

Error message

Expected a JSON object

What it means

After successfully parsing the file, _load requires the top-level JSON value to be a dict. A valid JSON array, string, number, or null raises ValueError('Expected a JSON object') (raised without the path). The library treats manifests as keyed objects only.

Solutions

  1. Wrap the content in an object with the expected key: {"assets": [...]}.
  2. Check the producing tool's output format and use its full manifest, not a partial export.
  3. Confirm the file starts with '{' not '[': head -c 1 manifest.json.

Example fix

// before
[{"id": "a", "modality": "image", "origin": "local", "path": "a.png"}]

// after
{"assets": [{"id": "a", "modality": "image", "origin": "local", "path": "a.png"}]}
Defensive patterns

Strategy: type-guard

Validate before calling

import json
data = json.load(open(path))
assert isinstance(data, dict), 'manifest root must be an object'

Type guard

def is_manifest(v) -> bool:
    return isinstance(v, dict) and isinstance(v.get('assets'), list)

Try / catch

try:
    data = _load_manifest(path)
except ValueError as e:
    if str(e) == 'Expected a JSON object':
        wrap_raw_list_into_object(path)

Prevention

When it happens

Trigger: A manifest file whose root is a JSON array (e.g., starts with '['), string, or number — parsed fine but not a dict — passed to _load via _bundle, ingest_assets, or validate_assets.

Common situations: Someone exported just the assets list instead of the full manifest object; a tool produced [{...},{...}] at top level; user wrote a bare array of assets to save typing.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/c894ff49fc9e4bb3. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/assets.py:84

    if len({identity(info) for info in (before, opened, after, final)}) != 1:
        raise ValueError(f'File changed while hashing: {path}')
    if modality == '3d_asset':
        if len(header) != 12:
            raise ValueError(f'Truncated GLB header: {path}')
        magic, version, size = struct.unpack('<4sII', header)
        if magic != b'glTF' or version != 2 or size != final.st_size:
            raise ValueError(f'Invalid GLB magic, version or declared size: {path}')
    return dict(path=str(path), bytes=final.st_size, sha256=digest.hexdigest())


def _load(path: Path) -> dict[str, Any]:
    _fingerprint(path)
    try:
        data = json.loads(path.read_text(encoding='utf-8'))
    except (ValueError, OSError) as exc:
        raise ValueError(f'Cannot read JSON object: {path}') from exc
    if not isinstance(data, dict):
        raise ValueError('Expected a JSON object')
    return data


def _assets(value: Any) -> list[dict[str, Any]]:
    if not isinstance(value, list) or not value:
        raise ValueError('assets must be a nonempty list')
    ids = set()
    for asset in value:
        if not isinstance(asset, dict):
            raise ValueError('Each asset must be an object')
        asset_id = _text(asset.get('id'), 'asset id')
        if asset_id in ids:
            raise ValueError(f'Duplicate asset id: {asset_id}')
        ids.add(asset_id)
        if asset.get('modality') not in tuple(MODALITIES):
            raise ValueError('modality must be image, video or 3d_asset')
        if asset.get('origin') not in tuple(ORIGINS):
            raise ValueError('Invalid asset origin')

View on GitHub (pinned to 8321021c54)