{"record":{"id":"d512407bad35052a","repo":"jaegertracing/jaeger","slug":"aggregation-bucket-has-a-non-string-key-w","errorCode":null,"errorMessage":"aggregation bucket has a non-string key: %w","messagePattern":"aggregation bucket has a non-string key: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/elasticsearch/esclient/aggregation.go","lineNumber":122,"sourceCode":"// and any nested sub-aggregations (reached through the promoted Aggregations\n// accessors).\ntype AggregationBucket struct {\n\tKey      string\n\tDocCount int\n\tAggregations\n}\n\nfunc (b *AggregationBucket) UnmarshalJSON(data []byte) error {\n\traw := map[string]json.RawMessage{}\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\t// Bucket keys are strings for the fields we aggregate (traceID, serviceName).\n\t// A present-but-non-string key means a mapping regression, so fail the decode\n\t// rather than silently yield an empty key that callers treat as a valid ID.\n\tif k, ok := raw[\"key\"]; ok {\n\t\tif err := json.Unmarshal(k, &b.Key); err != nil {\n\t\t\treturn fmt.Errorf(\"aggregation bucket has a non-string key: %w\", err)\n\t\t}\n\t\tdelete(raw, \"key\")\n\t}\n\tif dc, ok := raw[\"doc_count\"]; ok {\n\t\tif err := json.Unmarshal(dc, &b.DocCount); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdelete(raw, \"doc_count\")\n\t}\n\tb.Aggregations = raw\n\treturn nil\n}\n\nfunc (b AggregationBucket) MarshalJSON() ([]byte, error) {\n\treturn marshalBucket(b.Key, b.DocCount, b.Aggregations)\n}\n\n// HistogramResult holds the buckets of a date_histogram aggregation.","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/jaegertracing/jaeger/blob/806f4447841ecdb60519f408b004a599d515f437/internal/storage/elasticsearch/esclient/aggregation.go#L104-L140","documentation":"When decoding aggregation response buckets, Jaeger expects each bucket's 'key' to be a JSON string (aggregations are on traceID or serviceName). UnmarshalJSON fails the decode with this wrapped error if 'key' is present but not a string, instead of yielding an empty key that callers would treat as a valid ID.","triggerScenarios":"Calling json.Unmarshal/Decode on an aggregation response whose bucket objects contain a non-string 'key' (e.g. a number or object) while unmarshalling into aggregationBucket.","commonSituations":"Elasticsearch mapping regression so the aggregated field is indexed as a number/keyword-object; querying an unexpected field where keys are numeric.","solutions":["Fix the index mapping so the aggregated field is a keyword/string type","Reindex affected indices so bucket keys are strings","Check which field the aggregation targets and that the backend version emits string keys"],"exampleFix":"// before (mapping)\n\"serviceVersion\": { \"type\": \"long\" }\n// after\n\"serviceVersion\": { \"type\": \"keyword\" }","handlingStrategy":"try-catch","validationCode":"// before decoding, sanity-check mapping of the aggregated field\nvar m map[string]any\njson.Unmarshal(mappingJSON, &m)\n// ensure the aggregated field type is 'keyword', not numeric","typeGuard":"func isStringKey(raw json.RawMessage) bool {\n    var s string\n    return json.Unmarshal(raw, &s) == nil\n}","tryCatchPattern":"var buckets []aggregationBucket\nif err := json.Unmarshal(respBody, &buckets); err != nil {\n    if strings.Contains(err.Error(), \"non-string key\") {\n        // alert: index mapping regression on the aggregated field\n    }\n    return fmt.Errorf(\"decoding aggregation response: %w\", err)\n}","preventionTips":["Keep aggregated fields mapped as keyword in ES templates","Add integration tests asserting bucket key types","Monitor for mapping conflicts after index template changes"],"tags":["elasticsearch","json-decoding","aggregation"],"backgroundTag":"json-unmarshal-type-mismatch","analyzedSha":"806f4447841ecdb60519f408b004a599d515f437","analyzedAt":"2026-09-01T02:39:22.140Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}