{"record":{"id":"78020bf3cc05cf1e","repo":"can1357/oh-my-pi","slug":"field-must-be-a-list","errorCode":null,"errorMessage":"{field} must be a list","messagePattern":"(.+?) must be a list","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/omp-rpc/src/omp_rpc/protocol.py","lineNumber":176,"sourceCode":"def _clone_json_object(value: object, *, field: str) -> JsonObject:\n    if not isinstance(value, dict):\n        raise ValueError(f\"{field} must be an object\")\n    return cast(JsonObject, _clone_json_value(value, field=field))\n\n\ndef _optional_json_object(value: object, *, field: str) -> JsonObject | None:\n    if value is None:\n        return None\n    return _clone_json_object(value, field=field)\n\n\ndef _optional_json_objects(\n    values: object, *, field: str\n) -> tuple[JsonObject, ...] | None:\n    if values is None:\n        return None\n    if not isinstance(values, list):\n        raise ValueError(f\"{field} must be a list\")\n    return tuple(_clone_json_object(item, field=f\"{field}[]\") for item in values)\n\n\ndef _clone_json_objects(values: object, *, field: str) -> tuple[JsonObject, ...]:\n    if values is None:\n        return ()\n    if not isinstance(values, list):\n        raise ValueError(f\"{field} must be a list\")\n    return tuple(_clone_json_object(item, field=f\"{field}[]\") for item in values)\n\n\ndef _require_literal(value: object, allowed: frozenset[str], *, field: str) -> str:\n    if not isinstance(value, str) or value not in allowed:\n        expected = \", \".join(sorted(allowed))\n        raise ValueError(f\"{field} must be one of: {expected}\")\n    return value\n\n","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/omp-rpc/src/omp_rpc/protocol.py#L158-L194","documentation":"`_optional_json_objects` parses an optional field that must be a JSON array of objects, returning None when the value is None. It raises this ValueError when the value is present but is not a list — commonly because a single object was supplied instead of wrapping it in an array. Items are additionally validated as objects via `_clone_json_object` with field name `{field}[]`.","triggerScenarios":"`parse_extension_ui_request` receiving e.g. `\"options\": {\"a\": 1}` (a single object) or `\"options\": \"a,b\"` (a string) where a list of objects is required: `[{'a': 1}]`. Also occurs when the field is accidentally omitted-safe None but a truthy non-list like a dict is supplied.","commonSituations":"Client sends one element without array brackets; a config file specifies a map instead of a list; an older daemon/client version uses a different shape for the field; hand-written payloads in tests forgot the list wrapper.","solutions":["Wrap single objects in a list before parsing: pass `[obj]` instead of `obj`.","If the value may be absent, pass None (or omit it) rather than an empty dict, so the optional path returns None cleanly.","Compare the payload shape against the current protocol schema in omp_rpc/protocol.py to confirm the expected `list[object]` shape.","Check for client/daemon version mismatch if the field shape recently changed upstream."],"exampleFix":"// before\nparse_extension_ui_request({\"method\": \"select\", \"widgets\": {\"id\": 1}})\n// ValueError: widgets must be a list\n\n// after\nparse_extension_ui_request({\"method\": \"select\", \"widgets\": [{\"id\": 1}]})","handlingStrategy":"type-guard","validationCode":"def is_json_object_list(value) -> bool:\n    return value is None or (\n        isinstance(value, list)\n        and all(isinstance(item, dict) for item in value)\n    )\n\nassert is_json_object_list(payload.get(field)), f\"{field} must be a list of objects\"","typeGuard":"def is_object_list(value: object) -> bool:\n    return isinstance(value, list) and all(isinstance(item, dict) for item in value)","tryCatchPattern":"try:\n    req = parse_extension_ui_request(raw)\nexcept ValueError as exc:\n    if \"must be a list\" in str(exc):\n        # auto-wrap a lone object if that's the common caller mistake\n        raw[field] = [raw[field]] if isinstance(raw.get(field), dict) else []\n        req = parse_extension_ui_request(raw)\n    else:\n        raise","preventionTips":["Wrap single objects in a list at construction time — the field is always plural.","Prefer passing None over empty dicts for absent optional fields.","Check the payload shape against the protocol schema after any client upgrade.","Normalize tuples from other libraries with list(...) before parsing."],"tags":["python","validation","rpc","type-mismatch"],"backgroundTag":"schema-validation-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}