admin管理员组

文章数量:1401247

I'm writing a service using connectrpc.This protocol allows for connections to the service through either HTTP or gRPC. The gRPC handlers work fine, but for the HTTP handlers, I want to allow for custom JSON marshaling and unmarshling for some enum types. Currently, I have this:


// HealthCheck contains information about the health of a service.
type HealthCheck struct {
    state protoimpl.MessageState `protogen:"open.v1"`
    // version of the service.
    Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
    // status of the service.
    Status HealthStatus `protobuf:"varint,2,opt,name=status,proto3,enum=common.v1.HealthStatus" json:"status,omitempty"`
    // dependencies contains a mapping between the name of the dependency and its status.
    Dependencies  map[string]*DependencyStatus `protobuf:"bytes,3,rep,name=dependencies,proto3" json:"dependencies,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
    unknownFields protoimpl.UnknownFields
    sizeCache     protoimpl.SizeCache
}

// HealthStatusAlternates contains alternate values for the HealthStatus enum
var HealthStatusAlternates = map[string]HealthStatus{
    "":     HealthStatus_HEALTH_STATUS_UNSPECIFIED,
    "up":   HealthStatus_HEALTH_STATUS_UP,
    "down": HealthStatus_HEALTH_STATUS_DOWN,
}

// HealthStatusMapping contains alternate names for the HealthStatus enum
var HealthStatusMapping = map[HealthStatus]string{
    HealthStatus_HEALTH_STATUS_UNSPECIFIED: "",
    HealthStatus_HEALTH_STATUS_UP:          "up",
    HealthStatus_HEALTH_STATUS_DOWN:        "down",
}

// MarshalJSON converts a HealthStatus value to a JSON value
func (enum HealthStatus) MarshalJSON() ([]byte, error) {
    return []byte(utils.MarshalString(enum, HealthStatus_name, HealthStatusMapping, utils.DoubleQuotes)), nil
}

I'm also using protojson to attempt to set this up:

path, handler := registrar(inner,
    connectproto.WithJSON(
        protojson.MarshalOptions{AllowPartial: true, UseProtoNames: false, EmitUnpopulated: false, EmitDefaultValues: false},
        protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true},
    ),
)

mux := http.NewServeMux()
mux.Handle(path, handler)

h2cHandler := h2c.NewHandler(mux, &http2.Server{})


httpServerExitDone := new(sync.WaitGroup)
httpServerExitDone.Add(1)
srv := startHTTPServer(httpServerExitDone, h2cHandler)

<-ctx.Done()
if err := srv.Shutdown(ctx); err != nil {
    panic(err)
}

httpServerExitDone.Wait()
log.Printf("Shutdown of %s (%s) requested.", name, version)

However, when I attempt to call this service I expect the HealthCheck to serialize as {"version": "v1.0.0", "status": "up"} and instead I get {"version": "v1.0.0", "status": "HEALTH_STATUS_UP"}. Debugging into the code, I see that MarshalJSON isn't being called and protojson doesn't actually use it either. Is there any way I can enable this behavior?

I'm writing a service using connectrpc.This protocol allows for connections to the service through either HTTP or gRPC. The gRPC handlers work fine, but for the HTTP handlers, I want to allow for custom JSON marshaling and unmarshling for some enum types. Currently, I have this:


// HealthCheck contains information about the health of a service.
type HealthCheck struct {
    state protoimpl.MessageState `protogen:"open.v1"`
    // version of the service.
    Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"`
    // status of the service.
    Status HealthStatus `protobuf:"varint,2,opt,name=status,proto3,enum=common.v1.HealthStatus" json:"status,omitempty"`
    // dependencies contains a mapping between the name of the dependency and its status.
    Dependencies  map[string]*DependencyStatus `protobuf:"bytes,3,rep,name=dependencies,proto3" json:"dependencies,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
    unknownFields protoimpl.UnknownFields
    sizeCache     protoimpl.SizeCache
}

// HealthStatusAlternates contains alternate values for the HealthStatus enum
var HealthStatusAlternates = map[string]HealthStatus{
    "":     HealthStatus_HEALTH_STATUS_UNSPECIFIED,
    "up":   HealthStatus_HEALTH_STATUS_UP,
    "down": HealthStatus_HEALTH_STATUS_DOWN,
}

// HealthStatusMapping contains alternate names for the HealthStatus enum
var HealthStatusMapping = map[HealthStatus]string{
    HealthStatus_HEALTH_STATUS_UNSPECIFIED: "",
    HealthStatus_HEALTH_STATUS_UP:          "up",
    HealthStatus_HEALTH_STATUS_DOWN:        "down",
}

// MarshalJSON converts a HealthStatus value to a JSON value
func (enum HealthStatus) MarshalJSON() ([]byte, error) {
    return []byte(utils.MarshalString(enum, HealthStatus_name, HealthStatusMapping, utils.DoubleQuotes)), nil
}

I'm also using protojson to attempt to set this up:

path, handler := registrar(inner,
    connectproto.WithJSON(
        protojson.MarshalOptions{AllowPartial: true, UseProtoNames: false, EmitUnpopulated: false, EmitDefaultValues: false},
        protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true},
    ),
)

mux := http.NewServeMux()
mux.Handle(path, handler)

h2cHandler := h2c.NewHandler(mux, &http2.Server{})


httpServerExitDone := new(sync.WaitGroup)
httpServerExitDone.Add(1)
srv := startHTTPServer(httpServerExitDone, h2cHandler)

<-ctx.Done()
if err := srv.Shutdown(ctx); err != nil {
    panic(err)
}

httpServerExitDone.Wait()
log.Printf("Shutdown of %s (%s) requested.", name, version)

However, when I attempt to call this service I expect the HealthCheck to serialize as {"version": "v1.0.0", "status": "up"} and instead I get {"version": "v1.0.0", "status": "HEALTH_STATUS_UP"}. Debugging into the code, I see that MarshalJSON isn't being called and protojson doesn't actually use it either. Is there any way I can enable this behavior?

Share Improve this question asked Mar 25 at 9:34 Woody1193Woody1193 8,0508 gold badges61 silver badges123 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 1

The ProtoJSON format specifies “enum: The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values.”

This is also how protojson encoding is implemented.

So no, this would be a specification violation. If you want something else, you have to implement a new library.

It seems that in the actual code, the Status field of HealthCheck is of a type defined by proto, so the custom MarshalJSON method is not called. You can use type CustomHealthStatus HealthStatus and convert the Status field that needs to be serialized to CustomHealthStatus. Here is an example:


// CustomHealthStatus wraps HealthStatus for JSON marshaling
type CustomHealthStatus HealthStatus

// MarshalJSON converts a HealthStatus value to a JSON value
func (enum CustomHealthStatus ) MarshalJSON() ([]byte, error) {
    return []byte(utils.MarshalString(enum, HealthStatus_name, HealthStatusMapping, utils.DoubleQuotes)), nil
}

func main() {
    healthCheck := &HealthCheck{
        Version: "v1.0.0",
        Status:  HealthStatus_HEALTH_STATUS_UP,
    }

    customHealthCheck := struct {
        Version string             `json:"version"`
        Status  CustomHealthStatus `json:"status"`
    }{
        Version: healthCheck.Version,
        Status:  CustomHealthStatus(healthCheck.Status),
    }
    // marshalOptions := protojson.MarshalOptions{
    //  UseProtoNames: false,
    // }
    // jsonData, err := marshalOptions.Marshal(proto.Message(healthCheck))
    jsonData, err := json.Marshal(customHealthCheck)
    if err != nil {
        log.Fatalf("Failed to marshal HealthCheck: %v", err)
    }

    fmt.Printf("Serialized HealthCheck: %s\n", jsonData) // Serialized HealthCheck: {"version":"v1.0.0","status":"up"}
}

本文标签: goUsing custom encodingjson Marshaler and Unmarshaler interface with protojsonStack Overflow