{"record":{"id":"fc47ef73b2419ced","repo":"restic/restic","slug":"invalid-start-of-string-q","errorCode":null,"errorMessage":"invalid start of string: %q","messagePattern":"invalid start of string: %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/restic/id.go","lineNumber":87,"sourceCode":"func (id ID) MarshalJSON() ([]byte, error) {\n\tbuf := make([]byte, 2+hex.EncodedLen(len(id)))\n\n\tbuf[0] = '\"'\n\thex.Encode(buf[1:], id[:])\n\tbuf[len(buf)-1] = '\"'\n\n\treturn buf, nil\n}\n\n// UnmarshalJSON parses the JSON-encoded data and stores the result in id.\nfunc (id *ID) UnmarshalJSON(b []byte) error {\n\t// check string length\n\tif len(b) != len(`\"\"`)+hex.EncodedLen(idSize) {\n\t\treturn fmt.Errorf(\"invalid length for ID: %q\", b)\n\t}\n\n\tif b[0] != '\"' {\n\t\treturn fmt.Errorf(\"invalid start of string: %q\", b[0])\n\t}\n\n\t// Strip JSON string delimiters. The json.Unmarshaler contract says we get\n\t// a valid JSON value, so we don't need to check that b[len(b)-1] == '\"'.\n\tb = b[1 : len(b)-1]\n\n\t_, err := hex.Decode(id[:], b)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid ID: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// IDFromHash returns the ID for the hash.\nfunc IDFromHash(hash []byte) (id ID) {\n\tif len(hash) != idSize {\n\t\tpanic(\"invalid hash type, not enough/too many bytes\")","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/restic/restic/blob/a80be1478a4c537f8396e0db2b05120aa78f11e0/internal/restic/id.go#L69-L105","documentation":"ID.UnmarshalJSON checked the total length (66 bytes) but the first byte is not a '\"', so the value is not a JSON string. Per the json.Unmarshaler contract restic assumes it receives a well-formed JSON value, so this only happens when the bytes are malformed: leading whitespace plus a quote elsewhere, raw hex padded to 66 bytes, or binary garbage from a corrupted file. It signals input that never was valid JSON at that position.","triggerScenarios":"Feeding a json.Decoder output offset wrong (decoding from the middle of a document); corrupted index/snapshot files where bytes shifted; manually constructed []byte that includes a prefix (e.g., 'id:' or a space) before the quoted ID.","commonSituations":"Custom parsers slicing JSON buffers by assumption instead of using json.Decoder token flow; files corrupted by truncated writes or bad sync jobs.","solutions":["Use encoding/json APIs (json.Unmarshal, json.Decoder) to decode the enclosing structure instead of slicing raw bytes","For repository files, run restic check and rebuild-index to regenerate damaged indexes","Log the offending bytes to find what prefix is polluting the value"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if len(raw) == 66 && raw[0] != '\"' {\n    return fmt.Errorf(\"ID field is not a JSON string (missing quote): %q\", raw)\n}","typeGuard":"func isQuotedJSONString(raw []byte) bool { return len(raw) >= 2 && raw[0] == '\"' }","tryCatchPattern":null,"preventionTips":["Use json.Decoder/json.Unmarshal on whole documents instead of slicing byte buffers by offset","Validate JSON documents with a schema or shape check before feeding restic types","Regenerate damaged repository JSON rather than patching bytes"],"tags":["go","restic","json","parsing","validation"],"backgroundTag":null,"analyzedSha":"a80be1478a4c537f8396e0db2b05120aa78f11e0","analyzedAt":"2026-08-15T15:30:29.928Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}