Search code examples
goprotocol-bufferskratos

protobuf FieldMask unmarshalling in Go


So I'm using kratos framework with Go and my API definition is:

rpc UpdateMeeting (UpdateMeetingRequest) returns (UpdateMeetingReply) {
    option (google.api.http) = {
      patch: "/v1/meetings/{meeting_id}"
      body: "*"
    };
  };

And the corresponding kratos generated func

func _Meeting_UpdateMeeting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
    in := new(UpdateMeetingRequest)
    if err := dec(in); err != nil {
        return nil, err
    }
    if interceptor == nil {
        return srv.(MeetingServer).UpdateMeeting(ctx, in)
    }
    info := &grpc.UnaryServerInfo{
        Server:     srv,
        FullMethod: Meeting_UpdateMeeting_FullMethodName,
    }
    handler := func(ctx context.Context, req interface{}) (interface{}, error) {
        return srv.(MeetingServer).UpdateMeeting(ctx, req.(*UpdateMeetingRequest))
    }
    return interceptor(ctx, in, info, handler)
}

Now if I have the following as the message request I do not have any issues unmarshalling and the request actually lands on my UpdateMeeting method being called from Kratos generated method.

message UpdateMeetingRequest {

  string  meeting_id = 1;

  string updated_by = 2;

  MeetingMeta meeting_meta = 3;
  // more fields left out
}

message MeetingMeta {
  string meeting_link = 1;
  // more fields left out
}

But when I add FieldMask which I actually need to check which field is present and which isn't, I get this error -

message UpdateMeetingRequest {

  string  meeting_id = 1;

  string updated_by = 2;

  MeetingMeta meeting_meta = 3;

  google.protobuf.FieldMask update_mask = 4;
}


Error
{
    "code": 400,
    "reason": "CODEC",
    "message": "body unmarshal proto: (line 5:18): google.protobuf.FieldMask.paths contains invalid path: \"meeting_meta\"",
    "metadata": {}
}

Payload

{
  "meeting_meta": {
      "meeting_link": "ac"
  },
  "update_mask": "meeting_meta"
}

For the update mask tried: meeting_meta.meeting_link, meeting_meta, meeting_link all three with same or similar errors.

Edit: So with approach mentioned in the answer I was able to make it work by changing the update_mask. meeting_meta -> meetingMeta.

But running into same issue, i.e., "contains invalid path" if there are two fields in the payload with '_' in name. Example:

{
  
  "updated_by": "dsjhvg",
  "meeting_meta": {
      "meeting_link": "ac"
  },
  "update_mask": "updatedBy, meetingMeta"
}

Individually both these fields are being unmarshalled just fine. Combined I'm running into an error for the second path in the update_mask. For this payload it's meeting_meta, if I rearrange them then its updated_by.


Solution

  • The payload should be modified to:

    {
      "meeting_meta": {
          "meeting_link": "ac"
      },
      "update_mask": "meetingMeta"
    }
    

    ("update_mask": "meeting_meta" is replaced with "update_mask": "meetingMeta")

    According to JSON Encoding of Field Masks, fields name in each path are converted to/from lower-camel naming conventions.

    The source code below copied from the google.golang.org/protobuf/encoding/protojson package shows how the fields name is verified (see line 20, _ in the field name is not allowed):

     1  func (d decoder) unmarshalFieldMask(m protoreflect.Message) error {
     2      tok, err := d.Read()
     3      if err != nil {
     4          return err
     5      }
     6      if tok.Kind() != json.String {
     7          return d.unexpectedTokenError(tok)
     8      }
     9      str := strings.TrimSpace(tok.ParsedString())
    10      if str == "" {
    11          return nil
    12      }
    13      paths := strings.Split(str, ",")
    14  
    15      fd := m.Descriptor().Fields().ByNumber(genid.FieldMask_Paths_field_number)
    16      list := m.Mutable(fd).List()
    17  
    18      for _, s0 := range paths {
    19          s := strs.JSONSnakeCase(s0)
    20          if strings.Contains(s0, "_") || !protoreflect.FullName(s).IsValid() {
    21              return d.newError(tok.Pos(), "%v contains invalid path: %q", genid.FieldMask_Paths_field_fullname, s0)
    22          }
    23          list.Append(protoreflect.ValueOfString(s))
    24      }
    25      return nil
    26  }
    

    Answer to the second question.

    If you look at the error message carefully, you will see something like:

    google.protobuf.FieldMask.paths contains invalid path: " meetingMeta"
    

    You see that the reported invalid path is " meetingMeta". Pay attention to the leading blank space. It means that the package does not trim the path for you. So you should not add spaces between paths. The payload should be:

    {
      
      "updated_by": "dsjhvg",
      "meeting_meta": {
          "meeting_link": "ac"
      },
      "update_mask": "updatedBy,meetingMeta"
    }