jsongogrpc

Using custom encoding/json Marshaler and Unmarshaler interface with protojson


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?


Solution

  • 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.