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
- Wrap the content in an object with the expected key: {"assets": [...]}.
- Check the producing tool's output format and use its full manifest, not a partial export.
- 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
- Check the first character of the file is '{'
- Always export full manifests from tooling, not partial arrays
- Add a schema check (jsonschema) before invoking the library
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
- assets must be a nonempty list
- Cannot read JSON object
- Each asset must be an object
- Failed to read
- Invalid at : expected a JSON object
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)