{"record":{"id":"1c4a38032605ace9","repo":"grpc/grpc-go","slug":"failed-to-unmarshal-message-is-t-want-proto-mes","errorCode":null,"errorMessage":"failed to unmarshal, message is %T, want proto.Message","messagePattern":"failed to unmarshal, message is %T, want proto\\.Message","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"encoding/proto/proto.go","lineNumber":88,"sourceCode":"\t\t}\n\t\tdata = append(data, mem.SliceBuffer(buf))\n\t} else {\n\t\tpool := mem.DefaultBufferPool()\n\t\tbuf := pool.Get(size)\n\t\tif _, err := marshalOptions.MarshalAppend((*buf)[:0], vv); err != nil {\n\t\t\tpool.Put(buf)\n\t\t\treturn nil, err\n\t\t}\n\t\tdata = append(data, mem.NewBuffer(buf, pool))\n\t}\n\n\treturn data, nil\n}\n\nfunc (c *codecV2) Unmarshal(data mem.BufferSlice, v any) (err error) {\n\tvv := messageV2Of(v)\n\tif vv == nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal, message is %T, want proto.Message\", v)\n\t}\n\n\tbuf := data.MaterializeToBuffer(mem.DefaultBufferPool())\n\tdefer buf.Free()\n\t// TODO: Upgrade proto.Unmarshal to support mem.BufferSlice. Right now, it's not\n\t//  really possible without a major overhaul of the proto package, but the\n\t//  vtprotobuf library may be able to support this.\n\treturn proto.Unmarshal(buf.ReadOnlyData(), vv)\n}\n\nfunc messageV2Of(v any) proto.Message {\n\tswitch v := v.(type) {\n\tcase protoadapt.MessageV1:\n\t\treturn protoadapt.MessageV2Of(v)\n\tcase protoadapt.MessageV2:\n\t\treturn v\n\t}\n","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/encoding/proto/proto.go#L70-L106","documentation":"The default gRPC proto codec's Unmarshal was given a value that is not a protobuf message. Internally, messageV2Of() only accepts values implementing protoadapt.MessageV1 (old github.com/golang/protobuf generated types) or protoadapt.MessageV2 (google.golang.org/protobuf generated types); anything else returns nil and Unmarshal rejects it. The %T in the message shows the concrete Go type that was passed, which is the fastest clue to what went wrong.","triggerScenarios":"Calling client.RecvMsg(resp) / server-side handler srv.RecvMsg(req) where resp/req is a plain Go struct (not a generated proto message), a nil pointer, a map/slice, or a value type instead of a pointer to a generated message. Also occurs with a custom codec registration that routes the wrong type into the proto codec, or when a message generated by an incompatible/old protoc-gen-go is mixed with the modern runtime.","commonSituations":"Passing a hand-written struct to SendMsg/RecvMsg instead of a .proto-generated type; passing a value (not a pointer) so the interface is satisfied differently; accidentally importing the deprecated github.com/golang/protobuf with an incompatible generated code version; upgrading grpc-go / protobuf major versions without regenerating .pb.go files; using a generic any-shaped placeholder during local testing.","solutions":["Inspect the %T in the message and replace that argument with a pointer to a code-generated proto message (e.g. *mypb.FooRequest).","If you wrote the struct by hand, define a .proto file and regenerate with protoc-gen-go so the type implements proto.Message.","Ensure you pass a pointer (&resp), not a value, to RecvMsg.","If mixing old/new protobuf, regenerate all .pb.go with the current google.golang.org/protobuf and remove stale github.com/golang/protobuf types from the call site.","If you genuinely need a non-proto wire format, register a custom encoding.Codec and do not route those types through the default proto codec."],"exampleFix":"// before\nvar req MyPlainStruct\n_ = stream.RecvMsg(&req)\n\n// after\nvar req *mypb.MyMessage // generated by protoc-gen-go\n_ = stream.RecvMsg(req)","handlingStrategy":"type-guard","validationCode":"// Before RecvMsg/SendMsg, ensure the value is a proto.Message.\nimport \"google.golang.org/protobuf/proto\"\n\nfunc mustProto(v any) proto.Message {\n    if m, ok := v.(proto.Message); ok {\n        return m\n    }\n    panic(fmt.Sprintf(\"not a proto.Message: %T\", v))\n}\n\nvar req *mypb.Foo = &mypb.Foo{}\n_ = mustProto(req)","typeGuard":"import \"google.golang.org/protobuf/proto\"\n\nfunc isProtoMessage(v any) bool {\n    _, ok := v.(proto.Message)\n    return ok\n}\n\n// usage:\nif !isProtoMessage(req) {\n    return fmt.Errorf(\"req must be a proto.Message, got %T\", req)\n}","tryCatchPattern":"if err := stream.RecvMsg(resp); err != nil {\n    if strings.Contains(err.Error(), \"want proto.Message\") {\n        // programming error: wrong message type; do not retry.\n        return fmt.Errorf(\"codec type mismatch for %T: %w\", resp, err)\n    }\n    return err\n}","preventionTips":["Always pass pointers to protoc-gen-go-generated types to SendMsg/RecvMsg.","Keep a single protobuf runtime (google.golang.org/protobuf) and regenerate all .pb.go on upgrades.","In tests, assert req implements proto.Message before the first RPC."],"tags":["protobuf","codec","serialization","unmarshal"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}