You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 18:24:50