{"record":{"id":"e4eb626007ddb005","repo":"d2lang/d2","slug":"decode-icon-url-for-stable-diagram-hash-got-d-fi","errorCode":null,"errorMessage":"decode icon URL for stable diagram hash: got %d fields, want %d","messagePattern":"decode icon URL for stable diagram hash: got (.+?) fields, want (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"d2target/d2target.go","lineNumber":355,"sourceCode":"\tif i < len(b) && b[i] == ':' {\n\t\ti++\n\t}\n\tfor i < len(b) && (b[i] == ' ' || b[i] == '\\t' || b[i] == '\\r' || b[i] == '\\n') {\n\t\ti++\n\t}\n\tif i >= len(b) {\n\t\treturn 0, fmt.Errorf(\"decode icon URL for stable diagram hash: missing value\")\n\t}\n\treturn i, nil\n}\n\nfunc stableHashURL(raw json.RawMessage) ([]byte, error) {\n\tfields := make(map[string]json.RawMessage, len(legacyURLFieldOrder))\n\tif err := json.Unmarshal(raw, &fields); err != nil {\n\t\treturn nil, fmt.Errorf(\"decode icon URL for stable diagram hash: %w\", err)\n\t}\n\tif len(fields) != len(legacyURLFieldOrder) {\n\t\treturn nil, fmt.Errorf(\"decode icon URL for stable diagram hash: got %d fields, want %d\", len(fields), len(legacyURLFieldOrder))\n\t}\n\n\tvar out bytes.Buffer\n\tout.WriteByte('{')\n\tfor i, name := range legacyURLFieldOrder {\n\t\tvalue, ok := fields[name]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"decode icon URL for stable diagram hash: missing field %q\", name)\n\t\t}\n\t\tif i > 0 {\n\t\t\tout.WriteByte(',')\n\t\t}\n\t\tout.WriteByte('\"')\n\t\tout.WriteString(name)\n\t\tout.WriteString(`\":`)\n\t\tout.Write(value)\n\t}\n\tout.WriteByte('}')","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/d2lang/d2/blob/0d69dca6f532ceaeacd615d35d1eaa41a238ffdb/d2target/d2target.go#L337-L373","documentation":"After unmarshaling, stableHashURL verifies the icon URL object contains exactly the fields in legacyURLFieldOrder so the hash stays stable across struct versions. This error fires when the field count differs, indicating the raw JSON has extra, missing, or renamed fields relative to the expected legacy layout.","triggerScenarios":"Passing an icon URL JSON object with a different number of keys than len(legacyURLFieldOrder) — extra fields, missing fields, or a struct version mismatch between serializer and hashing code.","commonSituations":"Mixing versions of d2target-generated data (old caches, persisted diagrams hashed with newer code), or adding a field to the URL struct without updating legacyURLFieldOrder.","solutions":["Compare the raw object's keys against legacyURLFieldOrder (d2target/d2target.go:227) and remove unexpected fields or supply the missing ones","If you changed the URL struct, update legacyURLFieldOrder to match","Invalidate/rebuild cached persisted diagrams produced by older versions","Pin consistent d2 versions for serialization and hashing"],"exampleFix":"// before (extra field)\n{\"url\":\"https://x/i.svg\",\"height\":24}\n// after\n{\"url\":\"https://x/i.svg\"}","handlingStrategy":"validation","validationCode":"var fields map[string]json.RawMessage\nif err := json.Unmarshal(raw, &fields); err != nil {\n    return err\n}\nif len(fields) != len(legacyURLFieldOrder) {\n    return fmt.Errorf(\"field count %d != expected %d\", len(fields), len(legacyURLFieldOrder))\n}","typeGuard":"func hasExpectedFieldCount(raw json.RawMessage) bool {\n    var m map[string]json.RawMessage\n    if json.Unmarshal(raw, &m) != nil {\n        return false\n    }\n    return len(m) == len(legacyURLFieldOrder)\n}","tryCatchPattern":"hashed, err := stableHashURL(raw)\nif err != nil {\n    if strings.Contains(err.Error(), \"fields, want\") {\n        log.Printf(\"schema drift in icon URL JSON: %v\", err)\n    }\n    return err\n}","preventionTips":["Update legacyURLFieldOrder whenever the URL struct changes","Invalidate hashes/caches when the schema version changes","Keep serialization and hashing in the same module version"],"tags":["json","hashing","schema-mismatch","d2target"],"backgroundTag":"schema-validation-failed","analyzedSha":"0d69dca6f532ceaeacd615d35d1eaa41a238ffdb","analyzedAt":"2026-08-31T12:19:21.182Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}