如何在ConnectRPC中结合protojson实现枚举自定义JSON编解码?
解决ConnectRPC HTTP处理器中枚举自定义JSON编解码问题
问题根源
ConnectRPC的HTTP处理器默认使用protojson库进行序列化,而protojson内部会直接处理枚举的数值与proto定义的原名映射,不会自动调用你为枚举类型实现的MarshalJSON/UnmarshalJSON方法,这就是自定义逻辑不生效的原因。
解决方案
提供两种可行方案,根据你的需求选择:
方案1:基于标准JSON库实现自定义编解码器
这种方法通过替换Connect默认的JSON编解码器,使用Go标准encoding/json库序列化消息,从而触发枚举类型的自定义JSON方法。
步骤1:确保枚举类型正确实现JSON接口
首先确认你的HealthStatus枚举已经正确实现json.Marshaler和json.Unmarshaler接口:
type HealthStatus int32 const ( HEALTH_STATUS_UP HealthStatus = 0 HEALTH_STATUS_DOWN HealthStatus = 1 ) var healthStatusToString = map[HealthStatus]string{ HEALTH_STATUS_UP: "up", HEALTH_STATUS_DOWN: "down", } var stringToHealthStatus = map[string]HealthStatus{ "up": HEALTH_STATUS_UP, "down": HEALTH_STATUS_DOWN, } // MarshalJSON 自定义枚举序列化逻辑 func (s HealthStatus) MarshalJSON() ([]byte, error) { str, ok := healthStatusToString[s] if !ok { return json.Marshal(int32(s)) } return json.Marshal(str) } // UnmarshalJSON 自定义枚举反序列化逻辑 func (s *HealthStatus) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err != nil { var num int32 if err := json.Unmarshal(data, &num); err != nil { return err } *s = HealthStatus(num) return nil } val, ok := stringToHealthStatus[str] if !ok { return fmt.Errorf("invalid HealthStatus: %s", str) } *s = val return nil }
步骤2:实现自定义Connect编解码器
编写符合connect.Codec接口的自定义JSON编解码器:
import ( "encoding/json" "fmt" "connectrpc.com/connect" "google.golang.org/protobuf/proto" ) type customJSONCodec struct{} func (c customJSONCodec) Name() string { // 必须返回"json",匹配客户端Content-Type请求头 return "json" } func (c customJSONCodec) Marshal(msg any) ([]byte, error) { protoMsg, ok := msg.(proto.Message) if !ok { return nil, fmt.Errorf("not a proto message: %T", msg) } // 使用标准JSON库序列化,触发枚举自定义方法 return json.Marshal(protoMsg) } func (c customJSONCodec) Unmarshal(data []byte, msg any) error { protoMsg, ok := msg.(proto.Message) if !ok { return nil, fmt.Errorf("not a proto message: %T", msg) } return json.Unmarshal(data, protoMsg) }
步骤3:配置Connect服务使用自定义编解码器
创建Connect Handler时替换默认编解码器:
import ( "net/http" "connectrpc.com/connect" yourpb "your/proto/package/path" ) func main() { healthService := &yourpb.HealthServiceServerImpl{} handler := connect.NewHandler( yourpb.NewHealthServiceHandler(healthService), // 指定自定义编解码器 connect.WithCodec(&customJSONCodec{}), // 其他服务配置选项... ) http.Handle("/health.Check", handler) // 启动HTTP服务器 if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } }
方案2:基于protojson自定义枚举解析器
如果需要保留protojson的原生特性(如proto字段名映射、空值处理),可以通过实现protojson.EnumResolver接口自定义枚举的字符串映射,无需修改枚举类型的JSON方法。
步骤1:实现自定义枚举解析器
import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/reflect/protoreflect" ) type customEnumResolver struct{} // FindEnumByName 复用默认枚举查找逻辑 func (r customEnumResolver) FindEnumByName(name protoreflect.FullName) (protoreflect.EnumDescriptor, bool) { return protojson.DefaultEnumResolver.FindEnumByName(name) } // FindEnumByNumber 复用默认枚举数值查找逻辑 func (r customEnumResolver) FindEnumByNumber(enum protoreflect.EnumDescriptor, number protoreflect.EnumNumber) (protoreflect.EnumValueDescriptor, bool) { return protojson.DefaultEnumResolver.FindEnumByNumber(enum, number) } // EnumString 自定义HealthStatus枚举的字符串映射 func (r customEnumResolver) EnumString(enum protoreflect.EnumDescriptor, number protoreflect.EnumNumber) (string, bool) { // 只针对目标枚举生效 if enum.FullName() == "your.package.HealthStatus" { switch number { case 0: return "up", true case 1: return "down", true } } // 其他枚举使用默认行为 return protojson.DefaultEnumResolver.EnumString(enum, number) }
步骤2:创建带自定义解析器的protojson编解码器
type customProtoJSONCodec struct { marshalOpts protojson.MarshalOptions unmarshalOpts protojson.UnmarshalOptions } func (c customProtoJSONCodec) Name() string { return "json" } func (c customProtoJSONCodec) Marshal(msg any) ([]byte, error) { protoMsg, ok := msg.(proto.Message) if !ok { return nil, fmt.Errorf("not a proto message: %T", msg) } return c.marshalOpts.Marshal(protoMsg) } func (c customProtoJSONCodec) Unmarshal(data []byte, msg any) error { protoMsg, ok := msg.(proto.Message) if !ok { return nil, fmt.Errorf("not a proto message: %T", msg) } return c.unmarshalOpts.Unmarshal(data, protoMsg) }
步骤3:配置Connect服务使用该编解码器
func main() { healthService := &yourpb.HealthServiceServerImpl{} // 初始化带自定义解析器的编解码器 codec := customProtoJSONCodec{ marshalOpts: protojson.MarshalOptions{ Resolver: &customEnumResolver{}, // 根据需求设置其他选项,如Indent、UseEnumNumbers等 }, unmarshalOpts: protojson.UnmarshalOptions{ Resolver: &customEnumResolver{}, }, } handler := connect.NewHandler( yourpb.NewHealthServiceHandler(healthService), connect.WithCodec(codec), ) http.Handle("/health.Check", handler) // 启动服务器... }
验证要点
- 方案1需确保枚举类型的JSON方法签名正确(
MarshalJSON为值方法,UnmarshalJSON为指针方法)。 - 两种方案的编解码器
Name()方法必须返回"json",否则无法匹配客户端的application/json请求头。 - gRPC处理器不受影响,仍可正常使用原生protobuf二进制序列化。
内容的提问来源于stack exchange,提问作者Woody1193
相关产品推荐
相关产品推荐

