{"record":{"id":"ed3f637094cce07f","repo":"caddyserver/caddy","slug":"s-s-field-must-be-a-string-or-number","errorCode":null,"errorMessage":"%s: %s field must be a string or number","messagePattern":"(.+?): (.+?) field must be a string or number","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"caddy.go","lineNumber":302,"sourceCode":"}\n\n// indexConfigObjects recursively searches ptr for object fields named\n// \"@id\" and maps that ID value to the full configPath in the index.\n// This function is NOT safe for concurrent access; obtain a write lock\n// on currentCtxMu.\nfunc indexConfigObjects(ptr any, configPath string, index map[string]string) error {\n\tswitch val := ptr.(type) {\n\tcase map[string]any:\n\t\tfor k, v := range val {\n\t\t\tif k == idKey {\n\t\t\t\tvar idStr string\n\t\t\t\tswitch idVal := v.(type) {\n\t\t\t\tcase string:\n\t\t\t\t\tidStr = idVal\n\t\t\t\tcase float64: // all JSON numbers decode as float64\n\t\t\t\t\tidStr = fmt.Sprintf(\"%v\", idVal)\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\"%s: %s field must be a string or number\", configPath, idKey)\n\t\t\t\t}\n\t\t\t\tif existingPath, ok := index[idStr]; ok {\n\t\t\t\t\treturn fmt.Errorf(\"duplicate ID '%s' found at %s and %s\", idStr, existingPath, configPath)\n\t\t\t\t}\n\t\t\t\tindex[idStr] = configPath\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// traverse this object property recursively\n\t\t\terr := indexConfigObjects(val[k], path.Join(configPath, k), index)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase []any:\n\t\t// traverse each element of the array recursively\n\t\tfor i := range val {\n\t\t\terr := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)\n\t\t\tif err != nil {","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/caddy.go#L284-L320","documentation":"indexConfigObjects recursively walks the decoded JSON config and registers '@id' fields. When an '@id' value decodes to something other than string or float64 (JSON numbers decode as float64) — i.e. it was an object, array, boolean, or null — the walk aborts with '<path>: @id field must be a string or number', naming the exact config path of the offending node. Surfaced through 'indexing config' during config changes.","triggerScenarios":"JSON config containing \"@id\": {\"name\":\"x\"}, \"@id\": [1], \"@id\": true, or \"@id\": null at any depth; PUT/PATCH to /config/ that writes such a value; template engines rendering @id as a nested structure.","commonSituations":"Hand-edited JSON configs; YAML->JSON converters emitting booleans for flags mistakenly placed under @id; copy-paste where an @id block was pasted inside another @id.","solutions":["Go to the config path in the error message and change that '@id' to a plain unique string.","Grep your JSON for '\"@id\"' followed by {, [, true, false, or null.","If the ID must carry structure, flatten it into the string (e.g. \"group:item\")."],"exampleFix":"// before\n\"handle\": { \"@id\": { \"app\": \"foo\" } }\n\n// after\n\"handle\": { \"@id\": \"foo\" }","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func validIDType(v any) bool {\n    switch v.(type) {\n    case string, float64:\n        return true\n    }\n    return false\n}","tryCatchPattern":null,"preventionTips":["Keep @id values as plain strings.","In generators, render @id with %s formatting of a string variable.","Scan JSON for '\"@id\":' followed by non-scalar values before submitting."],"tags":["caddy","admin-api","config","id-index","validation"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}