{"record":{"id":"5ad54a1ba1278846","repo":"JuliusBrussee/caveman","slug":"text-w","errorCode":null,"errorMessage":"text: %w","messagePattern":"text: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/pixel/transform_gemini.go","lineNumber":351,"sourceCode":"\t\tb, err := json.Marshal(req.Tools)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\traw[\"tools\"] = b\n\t}\n\treturn json.Marshal(raw)\n}\n\nfunc (p *geminiPart) UnmarshalJSON(data []byte) error {\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tp.Extra = raw\n\tif v, ok := raw[\"text\"]; ok {\n\t\tvar s string\n\t\tif err := json.Unmarshal(v, &s); err != nil {\n\t\t\treturn fmt.Errorf(\"text: %w\", err)\n\t\t}\n\t\tp.Text = &s\n\t}\n\tif v, ok := raw[\"inline_data\"]; ok {\n\t\tvar inline geminiInlineData\n\t\tif err := json.Unmarshal(v, &inline); err != nil {\n\t\t\treturn fmt.Errorf(\"inline_data: %w\", err)\n\t\t}\n\t\tp.InlineData = &inline\n\t}\n\tif v, ok := raw[\"functionResponse\"]; ok {\n\t\tvar fr geminiFunctionResponse\n\t\tif err := json.Unmarshal(v, &fr); err != nil {\n\t\t\treturn fmt.Errorf(\"functionResponse: %w\", err)\n\t\t}\n\t\tp.FunctionResponse = &fr\n\t}\n\tif v, ok := raw[\"functionCall\"]; ok {","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/pixel/transform_gemini.go#L333-L369","documentation":"A gemini part type's UnmarshalJSON first decodes into a raw field map, stores it in Extra, then decodes the \"text\" key into a *string. If \"text\" is present but is not a JSON string (e.g. an object or number), the error is wrapped as \"text: %w\" and unmarshaling of the whole part fails.","triggerScenarios":"Unmarshaling a Gemini request/response whose content part contains \"text\" with a non-string value — e.g. {\"text\": {\"value\": \"hi\"}} or {\"text\": 42} — anywhere TransformGemini or response handling decodes parts.","commonSituations":"Migrating payloads from providers where content blocks are objects; code that accidentally nests the string under an object; generated clients emitting typed text fields.","solutions":["Ensure each part's \"text\" is a plain JSON string: {\"text\": \"message\"}.","Check the wrapped error for the JSON type that was found instead of a string.","If the payload carries structured text, move the structure into a separate field or serialize it to a string first."],"exampleFix":"// before\n{\"parts\": [{\"text\": {\"body\": \"hi\"}}]}\n// after\n{\"parts\": [{\"text\": \"hi\"}]}","handlingStrategy":"validation","validationCode":"var probe struct {\n    Parts []struct {\n        Text any `json:\"text\"`\n    } `json:\"parts\"`\n}\nif err := json.Unmarshal(body, &probe); err != nil {\n    return err\n}\nfor i, p := range probe.Parts {\n    if _, ok := p.Text.(string); p.Text != nil && !ok {\n        return fmt.Errorf(\"part %d: text must be a JSON string\", i)\n    }\n}","typeGuard":"func hasStringText(part map[string]json.RawMessage) bool {\n    v, ok := part[\"text\"]\n    if !ok {\n        return true\n    }\n    var s string\n    return json.Unmarshal(v, &s) == nil\n}","tryCatchPattern":"var req geminiGenerateContent\nif err := json.Unmarshal(body, &req); err != nil {\n    if strings.Contains(err.Error(), \"text:\") {\n        return fmt.Errorf(\"a part's \\\"text\\\" is not a JSON string: %w\", err)\n    }\n    return err\n}","preventionTips":["Emit part text as a plain JSON string, never a nested object or number.","Validate parts with a schema checker before unmarshaling into the typed struct.","Unmarshal into a permissive map first when porting payloads from other providers, then normalize."],"tags":["gemini","json","unmarshal","schema"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}