在go的protobuf中进行自定义json tag标记及使用

在使用grpc-gateway的时候,测试时发现HTTP接口返回给前端的json数据的字段格式很不统一,所以需要标准化protobuf->json的映射关系

原因

  1. proro的字段命名很不规范,有全小写的,有大驼峰/小驼峰/下划线等等
  2. 使用了默认的 protoc-gen-go 插件,生成的json tag会尝试小驼峰以及omitempty,但如果是纯小写或大驼峰,则不会改变

解决方法

不使用 protoc-gen-go

比如使用 gogo,就可以完全自定义json tag的命名
参考如下例子

1
2
3
4
5
6
7
8
9
import "github.com/gogo/protobuf/gogoproto/gogo.proto";

// Result example:
// type Post struct {
// Number int64 `protobuf:"bytes,1,opt,name=number,json=no1,proto3" json:"no2"`
// }
message Post {
int64 number = 1 [json_name="no1", (gogoproto.jsontag) = "no2"];
}

json_name + jsonpb

上个方案中已经出现了json_name,官方的说明如下,具体见参考链接

Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys.
If the json_name field option is specified, the specified value will be used as the key instead.
Parsers accept both the lowerCamelCase name (or the one specified by the json_name option) and the original proto field name.
null is an accepted value for all field types and treated as the default value of the corresponding field type.

json_name并不会影响 protoc-gen-go 生成的go结构体中的json tag,而是会在 protobuf tag 中生成指定的json name
它的用途是在protobuf->json时被应用,而要让它起作用,encoding/json包是不行的,它只认json tag。要使用 github.com/golang/protobuf/jsonpb
看如下例子理解
定义proto文件,强行在Blog中塞进了不同的字段命名格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax = "proto3";

package blog;

option go_package = "./;blog";

service BlogService {
rpc Get (GetRequest) returns (GetResponse) {}
}

message Blog {
int64 id = 1 [json_name = "myid"];
string titleName = 2;
string author_name = 3;
string img = 4;
int64 CountNum = 5;
}

message GetRequest {
int64 id = 1;
}
message GetResponse {
Blog data = 1;
}

执行命令 protoc --go_out=paths=source_relative:. jsontag.proto,生成pb文件中的 message Blog 对应的 Blog 结构体如下

  • json tag是和message定义完全一致的
  • 如果字段原本就是驼峰格式,那么默认情况下,protobuf中是不会额外出现json=xxx内容的
    • 见字段 img / titleName / CountNum
  • 如果字段不是驼峰格式,或者指定了 json_name,那么protobuf中会出现json=xxx内容
    • 见字段 id / author_name
1
2
3
4
5
6
7
8
9
10
11
type Blog struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Id int64 `protobuf:"varint,1,opt,name=id,json=myid,proto3" json:"id,omitempty"`
TitleName string `protobuf:"bytes,2,opt,name=titleName,proto3" json:"titleName,omitempty"`
AuthorName string `protobuf:"bytes,3,opt,name=author_name,json=authorName,proto3" json:"author_name,omitempty"`
Img string `protobuf:"bytes,4,opt,name=img,proto3" json:"img,omitempty"`
CountNum int64 `protobuf:"varint,5,opt,name=CountNum,proto3" json:"CountNum,omitempty"`
}

json序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import "github.com/golang/protobuf/jsonpb"

func main() {
b := blog.Blog{Id: 42, TitleName: "nothing", AuthorName: "who"}

m := jsonpb.Marshaler{
OrigName: false,
EnumsAsInts: false,
EmitDefaults: false,
Indent: "",
AnyResolver: nil,
}

fmt.Println(m.MarshalToString(&b))
// {"myid":"42","titleName":"nothing","authorName":"who"} <nil>

// 使用原始的 protobuf 字段名
m.OrigName = true
fmt.Println(m.MarshalToString(&b))
// {"id":"42","titleName":"nothing","author_name":"who"} <nil>

// 零值字段输出
m.EmitDefaults = true
fmt.Println(m.MarshalToString(&b))
// {"id":"42","titleName":"nothing","author_name":"who","img":"","CountNum":"0"} <nil>

// 零值字段输出,但使用 protobuf 的 json tag
m.OrigName = false
fmt.Println(m.MarshalToString(&b))
// {"myid":"42","titleName":"nothing","authorName":"who","img":"","CountNum":"0"} <nil>

// 自定义缩进字符
m.Indent = "|——"
fmt.Println(m.MarshalToString(&b))
// {
// |——"id": "42",
// |——"titleName": "nothing",
// |——"author_name": "who",
// |——"img": "",
// |——"CountNum": "0"
// } <nil>

// 直接输出字符串
fmt.Println(b.String())
// id:42 titleName:"nothing" author_name:"who"
}

具体怎么用,就可以选择了

参考链接

原文链接
Defining custom go struct tags for protobuf message fields
Language Guide (proto3)