grpc/grpc-go · error

proto: failed to marshal, message is %T, want proto.Message

Error message

proto: failed to marshal, message is %T, want proto.Message

What it means

The default proto codec's Marshal received a value that is neither a proto.MessageV1 (legacy github.com/golang/protobuf) nor a proto.MessageV2 (google.golang.org/protobuf), as determined by messageV2Of returning nil. gRPC's default wire format is protobuf; sending a non-proto Go value (a plain struct, map, string, etc.) triggers this on the sending side.

Source

Thrown at encoding/proto/proto.go:46

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/protoadapt"
)

// Name is the name registered for the proto compressor.
const Name = "proto"

func init() {
	encoding.RegisterCodecV2(&codecV2{})
}

// codec is a CodecV2 implementation with protobuf. It is the default codec for
// gRPC.
type codecV2 struct{}

func (c *codecV2) Marshal(v any) (data mem.BufferSlice, err error) {
	vv := messageV2Of(v)
	if vv == nil {
		return nil, fmt.Errorf("proto: failed to marshal, message is %T, want proto.Message", v)
	}

	// Important: if we remove this Size call then we cannot use
	// UseCachedSize in MarshalOptions below.
	size := proto.Size(vv)

	// MarshalOptions with UseCachedSize allows reusing the result from the
	// previous Size call. This is safe here because:
	//
	// 1. We just computed the size.
	// 2. We assume the message is not being mutated concurrently.
	//
	// Important: If the proto.Size call above is removed, using UseCachedSize
	// becomes unsafe and may lead to incorrect marshaling.
	//
	// For more details, see the doc of UseCachedSize:
	// https://pkg.go.dev/google.golang.org/protobuf/proto#MarshalOptions
	marshalOptions := proto.MarshalOptions{UseCachedSize: true}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Make sure every RPC request/response is a generated protobuf message type (import the generated *.pb.go and use its structs).
  2. If you intentionally want non-proto payloads, register a custom encoding.Codec (e.g. a JSON codec named "json") and set the :content-type metadata / WithDefaultCallOptions(ForceCodec(...)).
  3. If you must use legacy github.com/golang/protobuf messages, ensure that module is imported so protoadapt.MessageV1Of can adapt them.

Example fix

// before
type Ping struct{ Msg string } // not a proto message
client.Invoke(ctx, "/svc/Do", &Ping{Msg:"hi"}, &Pong{})

// after
// after generating pb.go: client.Invoke(ctx, "/svc/Do", &pb.Ping{Msg:"hi"}, &pb.Pong{})
Defensive patterns

Strategy: type-guard

Validate before calling

import (
    "google.golang.org/protobuf/proto"
    "google.golang.org/protobuf/protoadapt"
)

func isProtoMessage(v any) bool {
    switch v.(type) {
    case protoadapt.MessageV1, protoadapt.MessageV2, proto.Message:
        return true
    }
    return false
}

Type guard

func isMarshalableProto(v any) bool {
    switch v.(type) {
    case protoadapt.MessageV1, protoadapt.MessageV2:
        return true
    }
    return false
}

Try / catch

data, err := proto.Marshal(m) // or via codec
if err != nil { return fmt.Errorf("marshal: %w", err) }

Prevention

When it happens

Trigger: Using grpc.UnaryClientInterceptor or a generic client to call client.Invoke with a non-proto request struct; passing a *struct{} or a custom message type that does not embed proto message methods; mixing grpc-go with a hand-rolled codec but forgetting to register it with encoding.RegisterCodec.

Common situations: Beginner mistake of trying to send a Go map or plain struct over gRPC; generating stubs with an outdated protoc plugin that produced V1 messages but the binary also lacks the V1->V2 adapter; sending the empty interface value nil.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/a6526bdf6f6af260. Report an issue: GitHub.