在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的命名
参考如下例子

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中塞进了不同的字段命名格式

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
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序列化

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)

留下只言片语: