2

I am trying to use generics to convert a JSON object into a GRPC response that has an enum, i.e.:

type GRPCResponse {
    str string
    enu EnumType
}

type EnumType int32
const (
    Type1 EnumType = 0
    Type2 EnumType = 1
)

The function for unmarshalling looks like this:

func assertHTTPResponseOK[T any](t *testing.T, endpoint string) T {
    body, err := GetResponse(endpoint)

    var v T
    err := json.Unmarshal(body, &v)
    require.Nil(t, err)
    return v
}

And the code calling it looks like this:

assertHTTPResponseOK[*GRPCResponse](t, "some-endpoint")

The JSON object in question looks like this:

{"str":"hello", "enu": "Type2"}

and I am getting an error along the lines of:

json: cannot unmarshal string into Go struct field GRPCResponse.enu of type EnumType

From similar questions, I have seen that the usual advice is to use jsonpb.Unmarshal or protojson.Unmarshal instead of the typical json.Unmarshal.

In changing the Unmarshal function, I also had to change T to be protoreflect.ProtoMessage. However, this prevents me from passing a pointer to v to Unmarshal, because it is a pointer to the interface, not the interface. Of course, I also cannot pass in a nil pointer (not take the address of v).

So my questions are:

  1. Is there a way to have the pointer of this generic object satisfy the interface protoreflect.ProtoMessage?
  2. Is there a better Unmarshalling function that would better fit my problem?
Alaska
  • 341
  • 2
  • 9
  • I think your problem is related to marshaling enums. Try this pattern if it helps: https://gist.github.com/lummie/7f5c237a17853c031a57277371528e87 – Kaus2b Apr 19 '22 at 19:26
  • @Kaus2b unfortunately, I cannot add a custom UnmarshalJSON because it is in the api.pb.go file which we are not supposed to edit for GRPC. – Alaska Apr 19 '22 at 20:35

2 Answers2

2

Here's a generics-friendly proto unmarshaller that avoids passing the second type, at the cost of a reflective invoke to peek the type inside the pointer and call it's constructor.

        var msg T // Constrained to proto.Message

        // Peek the type inside T (as T= *SomeProtoMsgType)
        msgType := reflect.TypeOf(msg).Elem()

        // Make a new one, and throw it back into T
        msg = reflect.New(msgType).Interface().(T)

        errUnmarshal := proto.Unmarshal(body, msg)
Steve Gray
  • 450
  • 2
  • 4
1

I ended up passing in the object I am unmarshalling into.

obj := new(GRPCResponse)
assertHTTPResponseOK[*GRPCResponse](t, ctx, "some-endpoint", obj)
func assertHTTPResponseOK[T protoreflect.ProtoMessage](t *testing.T, ctx context.Context, endpoint string, object T) {
    body, err := GetResponse(endpoint)
    require.Nil(t, err)

    err = protojson.Unmarshal(body, object)
    require.Nil(t, err)
}
Alaska
  • 341
  • 2
  • 9