Search code examples
goreflectionprotocol-buffersunmarshallingproto

How to extract field from protobuf message without schema


According to this issue, the protoreflect package provides APIs for accessing the "unknown fields" of protobuf messages, but I don't see any way to use it if there isn't any existing schema. Basically, I want to perform a "weak decode", similar to what the JSON unmarshaller does if the output is of type map[string]interface{}.

The example from the documentation looks like this:

err := UnmarshalOptions{DiscardUnknown: true}.Unmarshal(b, m)

where b is the input byte slice and m is the output message, which needs to be initialised somehow, as you can see here. I was thinking that dynamicpb can be used for this purpose, but it doesn't look possible without having an existing MessageDescriptor and that's where I got stuck...


Solution

  • I was able to achieve this using the low level protowire package. Here is a full example, where I extract two fields of type uint64 (which happen to be assigned field numbers 4 and 5 in the original schema):

    import "google.golang.org/protobuf/encoding/protowire"
    
    func getData(src []byte) (creationTime, expiryTime uint64, err error) {
        remaining := src
        for len(remaining) > 0 {
            fieldNum, wireType, n := protowire.ConsumeTag(remaining)
            if n < 0 {
                return 0, 0, fmt.Errorf("failed to consume tag: %w", protowire.ParseError(n))
            }
            remaining = remaining[n:]
    
            switch fieldNum {
            case 4: // Expiry time
                if wireType != protowire.VarintType {
                    return 0, 0, fmt.Errorf("unexpected type for expiry time field: %d", wireType)
                }
                expiryTime, n = protowire.ConsumeVarint(remaining)
            case 5: // Creation time
                if wireType != protowire.VarintType {
                    return 0, 0, fmt.Errorf("unexpected type for creation time field: %d", wireType)
                }
                creationTime, n = protowire.ConsumeVarint(remaining)
            default:
                n = protowire.ConsumeFieldValue(fieldNum, wireType, remaining)
            }
            if n < 0 {
                return 0, 0, fmt.Errorf("failed to consume value for field %d: %w", fieldNum, protowire.ParseError(n))
            }
            remaining = remaining[n:]
        }
    
        return
    }