{"record":{"id":"962d48ebab4bca46","repo":"tirth8205/code-review-graph","slug":"expected-json-object","errorCode":null,"errorMessage":"expected JSON object","messagePattern":"expected JSON object","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"code_review_graph/uninstall.py","lineNumber":271,"sourceCode":"    index += 1\n    while index < len(tokens):\n        kind = tokens[index].kind\n        if kind == token.kind:\n            depth += 1\n        elif kind == closing:\n            depth -= 1\n            if depth == 0:\n                return index + 1\n        elif kind in (\"{\", \"[\"):\n            index = _skip_value(tokens, index)\n            continue\n        index += 1\n    raise ValueError(\"unterminated JSON container\")\n\n\ndef _object_members(tokens: Sequence[_Token], index: int) -> list[_Member]:\n    if index >= len(tokens) or tokens[index].kind != \"{\":\n        raise ValueError(\"expected JSON object\")\n    members: list[_Member] = []\n    cursor = index + 1\n    while cursor < len(tokens) and tokens[cursor].kind != \"}\":\n        if tokens[cursor].kind == \",\":  # trailing comma\n            cursor += 1\n            continue\n        key_token = tokens[cursor]\n        if key_token.kind != \"string\" or not isinstance(key_token.value, str):\n            raise ValueError(\"expected JSON object key\")\n        if cursor + 1 >= len(tokens) or tokens[cursor + 1].kind != \":\":\n            raise ValueError(\"expected colon after JSON object key\")\n        value_index = cursor + 2\n        value_end = _skip_value(tokens, value_index)\n        comma_index = value_end if (\n            value_end < len(tokens) and tokens[value_end].kind == \",\"\n        ) else None\n        members.append(\n            _Member(key_token.value, cursor, value_index, value_end, comma_index)","sourceCodeStart":253,"sourceCodeEnd":289,"githubUrl":"https://github.com/tirth8205/code-review-graph/blob/b58668751ab0c7670c078cf7cbd4d1f5b8e54f81/code_review_graph/uninstall.py#L253-L289","documentation":"Raised by _object_members when the token at the given index is not the '{' that opens a JSON object (or the index is past the end of the token stream). It means the JSONC path being edited assumes an object exists at that position, but the tokenizer found something else. This is part of the hand-rolled JSONC editing used to surgically remove keys from config files during uninstall.","triggerScenarios":"Calling the uninstall/path-removal API with a path whose parent component resolves to a non-object value, e.g. removing 'a.b' when 'a' is a string, number, or array. Also triggered by malformed JSONC where an object is truncated or the wrong token lands at the expected position.","commonSituations":"User hand-edited a JSON config (settings.json, tsconfig-like file) and left it structurally broken; config schema changed between install and uninstall; path was built with a wrong key type (int where an object key was expected); comments/strings confused a hand-edit and braces got unbalanced.","solutions":["Validate the JSONC file parses as JSON (after stripping comments) before running the uninstall removal","Check the path components: every intermediate component must address an object (str key) or array (int index) matching the actual document structure","If the file was hand-edited, fix the unbalanced/missing braces so the target parent is actually an object","Catch the ValueError and fall back to leaving the file untouched (or rewriting it wholesale) instead of crashing the uninstall"],"exampleFix":"# before\nremove_jsonc_paths(text, [\"mcpServers\", \"code-review-graph\"])\n# with {\"mcpServers\": \"disabled\"}  -> ValueError: expected JSON object\n\n# after\nimport json, re\ndef strip_comments(s):\n    return re.sub(r\"//[^\\n]*|/\\*.*?\\*/\", \"\", s, flags=re.S)\ntry:\n    cfg = json.loads(strip_comments(text))\n    assert isinstance(cfg.get(\"mcpServers\"), dict)\nexcept (ValueError, AssertionError):\n    cfg = {}\nremove_jsonc_paths(text, [\"mcpServers\", \"code-review-graph\"])","handlingStrategy":"validation","validationCode":"import json, re\ndef strip_comments(s: str) -> str:\n    return re.sub(r\"//[^\\n]*|/\\*.*?\\*/\", \"\", s, flags=re.S)\n\ndef parent_is_object(text: str, path: list) -> bool:\n    try:\n        doc = json.loads(strip_comments(text))\n    except ValueError:\n        return False\n    cur = doc\n    for c in path:\n        try:\n            cur = cur[c]\n        except (KeyError, IndexError, TypeError):\n            return False\n    return isinstance(cur, dict)","typeGuard":"def is_json_object_root(text: str) -> bool:\n    t = text.lstrip()\n    return t.startswith('{')","tryCatchPattern":"try:\n    _remove_jsonc_paths(text, paths)\nexcept ValueError as e:\n    log.warning(\"skipping malformed config: %s\", e)  # leave file untouched","preventionTips":["Strip comments and json.loads the file before editing","Keep intermediate path components type-correct (str for objects, int for arrays)","Never hand-edit generated configs while an uninstall is in flight"],"tags":["json","jsonc","config-editing","uninstall","valueerror"],"backgroundTag":"json-parse-error","analyzedSha":"b58668751ab0c7670c078cf7cbd4d1f5b8e54f81","analyzedAt":"2026-08-28T13:19:08.966Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}