diff --git a/encoding/protojson/decode.go b/encoding/protojson/decode.go index bb2966e3b..fddb3431b 100644 --- a/encoding/protojson/decode.go +++ b/encoding/protojson/decode.go @@ -6,6 +6,7 @@ package protojson import ( "encoding/base64" + "encoding/hex" "fmt" "math" "strconv" @@ -41,6 +42,10 @@ type UnmarshalOptions struct { // If DiscardUnknown is set, unknown fields and enum name values are ignored. DiscardUnknown bool + // If UseHexForBytes is set, bytes fields are un-marshaled as hex + // strings instead of base64. + UseHexForBytes bool + // Resolver is used for looking up types when unmarshaling // google.protobuf.Any messages or extension fields. // If nil, this defaults to using protoregistry.GlobalTypes. @@ -338,8 +343,14 @@ func (d decoder) unmarshalScalar(fd protoreflect.FieldDescriptor) (protoreflect. } case protoreflect.BytesKind: - if v, ok := unmarshalBytes(tok); ok { - return v, nil + if d.opts.UseHexForBytes { + if v, ok := unmarshalBytesFromHex(tok); ok { + return v, nil + } + } else { + if v, ok := unmarshalBytes(tok); ok { + return v, nil + } } case protoreflect.EnumKind: @@ -488,6 +499,19 @@ func unmarshalBytes(tok json.Token) (protoreflect.Value, bool) { return protoreflect.ValueOfBytes(b), true } +func unmarshalBytesFromHex(tok json.Token) (protoreflect.Value, bool) { + if tok.Kind() != json.String { + return protoreflect.Value{}, false + } + + s := tok.ParsedString() + b, err := hex.DecodeString(s) + if err != nil { + return protoreflect.Value{}, false + } + return protoreflect.ValueOfBytes(b), true +} + func unmarshalEnum(tok json.Token, fd protoreflect.FieldDescriptor, discardUnknown bool) (protoreflect.Value, bool) { switch tok.Kind() { case json.String: diff --git a/encoding/protojson/encode.go b/encoding/protojson/encode.go index 29846df22..b61dab51b 100644 --- a/encoding/protojson/encode.go +++ b/encoding/protojson/encode.go @@ -6,6 +6,7 @@ package protojson import ( "encoding/base64" + "encoding/hex" "fmt" "google.golang.org/protobuf/internal/encoding/json" @@ -102,6 +103,10 @@ type MarshalOptions struct { // a strict superset of the latter. EmitDefaultValues bool + // If UseHexForBytes is set, bytes fields are marshaled as hex strings + // instead of base64. + UseHexForBytes bool + // Resolver is used for looking up types when expanding google.protobuf.Any // messages. If nil, this defaults to using protoregistry.GlobalTypes. Resolver interface { @@ -324,7 +329,13 @@ func (e encoder) marshalSingular(val protoreflect.Value, fd protoreflect.FieldDe e.WriteFloat(val.Float(), 64) case protoreflect.BytesKind: - e.WriteString(base64.StdEncoding.EncodeToString(val.Bytes())) + var encoded string + if e.opts.UseHexForBytes { + encoded = hex.EncodeToString(val.Bytes()) + } else { + encoded = base64.StdEncoding.EncodeToString(val.Bytes()) + } + e.WriteString(encoded) case protoreflect.EnumKind: if fd.Enum().FullName() == genid.NullValue_enum_fullname {