Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] 请问如何设置 proto3 默认值,零值字段被忽略,导致 http api 接口字段缺失 #1952

Closed
mowocc opened this issue Apr 25, 2022 · 20 comments
Labels
question Further information is requested

Comments

@mowocc
Copy link

mowocc commented Apr 25, 2022

请问如何设置 proto3 默认值,零值字段被忽略,导致 api 接口字段缺失。
比如用户余额接口,余额为0时(balance float64),接口余额字段缺失,这在对外提供的 http api 接口情况下,是无法接受。
请问在 kratos 中有解决方案吗?

@mowocc mowocc added the question Further information is requested label Apr 25, 2022
@fifsky
Copy link
Contributor

fifsky commented Apr 25, 2022

不会啊,kratos里面默认是发射proto协议的零值,配置如下
https://github.com/go-kratos/kratos/blob/main/encoding/json/json.go#L18
除非你用的不是kratos的encoding json

@mowocc
Copy link
Author

mowocc commented Apr 26, 2022

因为自定义了 ResponseEncoder,kratos的encoding json会选择使用默认的encoding/json,情况类似:#1539
最后的解决方案是:自定义 Response struct 同样使用 proto3 定义,data 定义为 google.protobuf.Any,xxxReply 赋值给 data

message Response {
  int32 code = 1;
  string message = 2;

  google.protobuf.Any data = 3;
}

代码大致如下:

func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
	codec, _ := kHttp.CodecForRequest(r, "Accept")
	rsp := &Response{
		Code:    http.StatusOK,
		Message: http.StatusText(http.StatusOK),
		//Data: v,
	}
	if m, ok := v.(proto.Message); ok {
		any, err := anypb.New(m)
		if err != nil {
			return err
		}
		rsp.Data = any
	}
	data, err := MarshalOptions.Marshal(rsp)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", ContentType(codec.Name()))
	_, err = w.Write(data)
	if err != nil {
		return err
	}
	return nil
}

但这种方案不够优雅,同时 data 中会出现 "@type": "type.googleapis.com/api.order.v3.OrderReply" 字段,是否有更好的解决方案?

同时思考一个问题,在项目 Response 结构已经确定,无法修改的情况下,kratos 写死结构的做法确实不够灵活,即使这是使用 google 推荐的方式,但在对接遗留的老项目、或者考虑接口兼容性,这样的情况会让大部分项目放弃使用 kratos,这种情况在 kratos 的其它模块设计中也同样存在。

@daemon365
Copy link
Member

因为自定义了 ResponseEncoder,kratos的encoding json会选择使用默认的encoding/json,情况类似:#1539 最后的解决方案是:自定义 Response struct 同样使用 proto3 定义,data 定义为 google.protobuf.Any,xxxReply 赋值给 data 代码大致如下:

func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
	codec, _ := kHttp.CodecForRequest(r, "Accept")
	rsp := &Response{
		Code:    http.StatusOK,
		Message: http.StatusText(http.StatusOK),
		//Data: v,
	}
	if m, ok := v.(proto.Message); ok {
		any, err := anypb.New(m)
		if err != nil {
			return err
		}
		rsp.Data = any
	}
	data, err := MarshalOptions.Marshal(rsp)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", ContentType(codec.Name()))
	_, err = w.Write(data)
	if err != nil {
		return err
	}
	return nil
}

但这种方案不够优雅,同时 data 中会出现 "@type": "type.googleapis.com/api.order.v3.OrderReply" 字段,是否有更好的解决方案?

同时思考一个问题,在项目 Response 结构已经确定,无法修改的情况下,kratos 写死结构的做法确实不够灵活,即使这是使用 google 推荐的方式,但在对接遗留的老项目、或者考虑接口兼容性,这样的情况会让大部分项目放弃使用 kratos,这种情况在 kratos 的其它模块设计中也同样存在。

这个是 protobuf 的问题 我们也想解决,但是解决不掉。
目前提供一种拼接byte数组的方式 可以参考一下

func(w nethttp.ResponseWriter, r *nethttp.Request, v interface{}) error {

    codec, _ := http.CodecForRequest(r, "Accept")
    data, err := codec.Marshal(v)
    if err != nil {
     return err
    }
    w.WriteHeader(nethttp.StatusOK)

    var reply *ResponseBody
    if err == nil {
     reply = &ResponseBody{
      Code: 0,
      Msg:  "",
     }
    } else {
     reply = &ResponseBody{
      Code: 500,
      Msg:  "",
     }
     var target *errors.Error
     if errors.As(err, target) {
      reply.Code = int32(target.Code)
      reply.Msg = target.Message
     }
    }

    replyData, err := codec.Marshal(reply)
    if err != nil {
     return err
    }

    var newData = make([]byte, 0, len(replyData)+len(data)+8)
    newData = append(newData, replyData[:len(replyData)-1]...)
    newData = append(newData, []byte(`,"data":`)...)
    newData = append(newData, data...)
    newData = append(newData, '}')

    w.Header().Set("Content-Type", "application/json")
    _, err = w.Write(newData)
    if err != nil {
     return err
    }
    return nil
   }),

@shenqidebaozi
Copy link
Member

shenqidebaozi commented Apr 26, 2022

kratos并没有写死结构,是你自己的 proto 声明,proto生成出来的struct结构体本来就带省略的tag,你自己包装结构体没有使用protojson,不是本就如此吗。我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题。可以考虑拼接字符串,或者在网关上加通用的结构,通过网关去做结构上的兼容

@mowocc mowocc closed this as completed Apr 26, 2022
@mowocc
Copy link
Author

mowocc commented Apr 26, 2022

因为自定义了 ResponseEncoder,kratos的encoding json会选择使用默认的encoding/json,情况类似:#1539 最后的解决方案是:自定义 Response struct 同样使用 proto3 定义,data 定义为 google.protobuf.Any,xxxReply 赋值给 data 代码大致如下:

func ResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
	codec, _ := kHttp.CodecForRequest(r, "Accept")
	rsp := &Response{
		Code:    http.StatusOK,
		Message: http.StatusText(http.StatusOK),
		//Data: v,
	}
	if m, ok := v.(proto.Message); ok {
		any, err := anypb.New(m)
		if err != nil {
			return err
		}
		rsp.Data = any
	}
	data, err := MarshalOptions.Marshal(rsp)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", ContentType(codec.Name()))
	_, err = w.Write(data)
	if err != nil {
		return err
	}
	return nil
}

但这种方案不够优雅,同时 data 中会出现 "@type": "type.googleapis.com/api.order.v3.OrderReply" 字段,是否有更好的解决方案?
同时思考一个问题,在项目 Response 结构已经确定,无法修改的情况下,kratos 写死结构的做法确实不够灵活,即使这是使用 google 推荐的方式,但在对接遗留的老项目、或者考虑接口兼容性,这样的情况会让大部分项目放弃使用 kratos,这种情况在 kratos 的其它模块设计中也同样存在。

这个是 protobuf 的问题 我们也想解决,但是解决不掉。 目前提供一种拼接byte数组的方式 可以参考一下

func(w nethttp.ResponseWriter, r *nethttp.Request, v interface{}) error {

    codec, _ := http.CodecForRequest(r, "Accept")
    data, err := codec.Marshal(v)
    if err != nil {
     return err
    }
    w.WriteHeader(nethttp.StatusOK)

    var reply *ResponseBody
    if err == nil {
     reply = &ResponseBody{
      Code: 0,
      Msg:  "",
     }
    } else {
     reply = &ResponseBody{
      Code: 500,
      Msg:  "",
     }
     var target *errors.Error
     if errors.As(err, target) {
      reply.Code = int32(target.Code)
      reply.Msg = target.Message
     }
    }

    replyData, err := codec.Marshal(reply)
    if err != nil {
     return err
    }

    var newData = make([]byte, 0, len(replyData)+len(data)+8)
    newData = append(newData, replyData[:len(replyData)-1]...)
    newData = append(newData, []byte(`,"data":`)...)
    newData = append(newData, data...)
    newData = append(newData, '}')

    w.Header().Set("Content-Type", "application/json")
    _, err = w.Write(newData)
    if err != nil {
     return err
    }
    return nil
   }),

感谢回复,这也是一种解决方案

@mowocc
Copy link
Author

mowocc commented Apr 26, 2022

我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题

拼接byte数组的方式已经理解,https://github.com/go-kratos/gateway 可以添加通用结构吗? 请问有比较通用的处理方式吗?

@mowocc
Copy link
Author

mowocc commented Apr 26, 2022

kratos并没有写死结构,是你自己的 proto 声明,proto生成出来的struct结构体本来就带省略的tag,你自己包装结构体没有使用protojson,不是本就如此吗。我觉得这是没有正确使用 proto 带来的副作用,因为你的外部结构体是强加在 proto 上的,就要承受相应的副作用问题。可以考虑拼接字符串,或者在网关上加通用的结构,通过网关去做结构上的兼容

感谢回复,最终采用了字符串拼接的方式。

@guihouchang
Copy link
Contributor

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"msg"`
	Data    interface{} `json:"data"`
}

func ResponseEncoder(w stdHttp.ResponseWriter, r *stdHttp.Request, v interface{}) error {
	reply := &Response{}
	reply.Code = 20000
	reply.Message = "success"

	codec, _ := http.CodecForRequest(r, "Accept")
	data, err := codec.Marshal(v)
	_ = json.Unmarshal(data, &reply.Data)
	if err != nil {
		return err
	}

	data, err = codec.Marshal(reply)
	if err != nil {
		return err
	}

	w.Header().Set("Content-Type", contentType(codec.Name()))
	w.WriteHeader(stdHttp.StatusOK)
	w.Write(data)
	return nil
}

这样子会不会好一点的呢? 只不过要多进行一次序列化

@Xwudao
Copy link

Xwudao commented Jun 14, 2022

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"msg"`
	Data    interface{} `json:"data"`
}

func ResponseEncoder(w stdHttp.ResponseWriter, r *stdHttp.Request, v interface{}) error {
	reply := &Response{}
	reply.Code = 20000
	reply.Message = "success"

	codec, _ := http.CodecForRequest(r, "Accept")
	data, err := codec.Marshal(v)
	_ = json.Unmarshal(data, &reply.Data)
	if err != nil {
		return err
	}

	data, err = codec.Marshal(reply)
	if err != nil {
		return err
	}

	w.Header().Set("Content-Type", contentType(codec.Name()))
	w.WriteHeader(stdHttp.StatusOK)
	w.Write(data)
	return nil
}

这样子会不会好一点的呢? 只不过要多进行一次序列化

还是有零值问题呀

@guihouchang
Copy link
Contributor

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"msg"`
	Data    interface{} `json:"data"`
}

func ResponseEncoder(w stdHttp.ResponseWriter, r *stdHttp.Request, v interface{}) error {
	reply := &Response{}
	reply.Code = 20000
	reply.Message = "success"

	codec, _ := http.CodecForRequest(r, "Accept")
	data, err := codec.Marshal(v)
	_ = json.Unmarshal(data, &reply.Data)
	if err != nil {
		return err
	}

	data, err = codec.Marshal(reply)
	if err != nil {
		return err
	}

	w.Header().Set("Content-Type", contentType(codec.Name()))
	w.WriteHeader(stdHttp.StatusOK)
	w.Write(data)
	return nil
}

这样子会不会好一点的呢? 只不过要多进行一次序列化

还是有零值问题呀

这个利用到pbjson库,但是前提要设置

json.MarshalOptions = protojson.MarshalOptions{
		EmitUnpopulated: true,
		UseProtoNames:   true,
	}

@shenqidebaozi
Copy link
Member

image

@doing-cr7
Copy link

20230329 网上的方法都试遍了 还是使用这个字节拼接的方式 靠谱

@kratos-ci-bot
Copy link
Collaborator

Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿


20230329 I have tried all the methods on the Internet, but I still use this byte splicing method, which is reliable

@zongjiye
Copy link

zongjiye commented Apr 12, 2023

type httpResponse struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

// ApiErrorEncoder 错误响应封装
func ApiErrorEncoder() http.EncodeErrorFunc {
	return func(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {
		if err == nil {
			return
		}
		se := &httpResponse{}
		gs, ok := status.FromError(err)
		if !ok {
			se = &httpResponse{Code: stdhttp.StatusInternalServerError}
		}
		se = &httpResponse{
			Code:    httpstatus.FromGRPCCode(gs.Code()),
			Message: gs.Message(),
			Data:    nil,
		}
		codec, _ := http.CodecForRequest(r, "Accept")
		body, err := codec.Marshal(se)
		if err != nil {
			w.WriteHeader(stdhttp.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/"+codec.Name())
		w.WriteHeader(se.Code)
		_, _ = w.Write(body)
	}
}

// ApiResponseEncoder  请求响应封装
func ApiResponseEncoder() http.EncodeResponseFunc {
	return func(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error {
		if v == nil {
			return nil
		}
		resp := &httpResponse{
			Code:    stdhttp.StatusOK,
			Message: stdhttp.StatusText(stdhttp.StatusOK),
		}
		codec := encoding.GetCodec("json")
		respData, err := codec.Marshal(resp)
		if err != nil {
			return err
		}

		data, err := codec.Marshal(v)
		if err != nil {
			return err
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(stdhttp.StatusOK)
		_, err = w.Write(bytes.Replace(respData, []byte("null"), data, 1))
		if err != nil {
			return err
		}
		return nil
	}
}```

看了一下上面还是觉得字符拼接或者替换还是更靠谱些,改成替换优雅点

@jmolboy
Copy link

jmolboy commented Jul 19, 2023

type httpResponse struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

// ApiErrorEncoder 错误响应封装
func ApiErrorEncoder() http.EncodeErrorFunc {
	return func(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {
		if err == nil {
			return
		}
		se := &httpResponse{}
		gs, ok := status.FromError(err)
		if !ok {
			se = &httpResponse{Code: stdhttp.StatusInternalServerError}
		}
		se = &httpResponse{
			Code:    httpstatus.FromGRPCCode(gs.Code()),
			Message: gs.Message(),
			Data:    nil,
		}
		codec, _ := http.CodecForRequest(r, "Accept")
		body, err := codec.Marshal(se)
		if err != nil {
			w.WriteHeader(stdhttp.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "application/"+codec.Name())
		w.WriteHeader(se.Code)
		_, _ = w.Write(body)
	}
}

// ApiResponseEncoder  请求响应封装
func ApiResponseEncoder() http.EncodeResponseFunc {
	return func(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error {
		if v == nil {
			return nil
		}
		resp := &httpResponse{
			Code:    stdhttp.StatusOK,
			Message: stdhttp.StatusText(stdhttp.StatusOK),
		}
		codec := encoding.GetCodec("json")
		respData, err := codec.Marshal(resp)
		if err != nil {
			return err
		}

		data, err := codec.Marshal(v)
		if err != nil {
			return err
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(stdhttp.StatusOK)
		_, err = w.Write(bytes.Replace(respData, []byte("null"), data, 1))
		if err != nil {
			return err
		}
		return nil
	}
}```

看了一下上面还是觉得字符拼接或者替换还是更靠谱些,改成替换优雅点

我这里报异常:

{
    "code": 500,
    "reason": "",
    "message": "failed to marshal, message is *server.httpResponse, want proto.Message",
    "metadata": {}
}

@kvii
Copy link
Contributor

kvii commented Nov 22, 2023

中间件定义看我这个就行了,嘎嘎好使。

不过有一说一,能从 "code message data" 格式中迁移出来的就尽量迁移出来吧,搞那些玩意儿干啥。每个后端的 "code message data" 的定义都不一样,连累前端挨个写 axios 中间件。都用上 kratos 了,这些老观念该抛弃就抛弃了吧。

package server

import (
	v1 "code_msg_data/api/helloworld/v1"
	"code_msg_data/internal/conf"
	"code_msg_data/internal/service"
	sj "encoding/json"
	nt "net/http"
	"strings"

	"github.com/go-kratos/kratos/v2/encoding"
	"github.com/go-kratos/kratos/v2/encoding/json"
	"github.com/go-kratos/kratos/v2/errors"
	"github.com/go-kratos/kratos/v2/log"
	"github.com/go-kratos/kratos/v2/middleware/recovery"
	"github.com/go-kratos/kratos/v2/transport/http"
)

// 最终效果
// $ curl http://localhost:8000/helloworld/kvii
// {"code":0,"message":"success","data":{"message":"Hello kvii"}}

// $ curl http://localhost:8000/helloworld/err
// {"code":404,"message":"user not found"}

// NewHTTPServer new an HTTP server.
func NewHTTPServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *http.Server {
	var opts = []http.ServerOption{
		http.Middleware(
			recovery.Recovery(),
		),
		http.ErrorEncoder(DefaultErrorEncoder),       // <- 关键代码
		http.ResponseEncoder(DefaultResponseEncoder), // <- 关键代码
	}
	if c.Http.Network != "" {
		opts = append(opts, http.Network(c.Http.Network))
	}
	if c.Http.Addr != "" {
		opts = append(opts, http.Address(c.Http.Addr))
	}
	if c.Http.Timeout != nil {
		opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
	}
	srv := http.NewServer(opts...)
	v1.RegisterGreeterHTTPServer(srv, greeter)
	return srv
}

// DefaultResponseEncoder copy from http.DefaultResponseEncoder
func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
	if v == nil {
		return nil
	}
	if rd, ok := v.(http.Redirector); ok {
		url, code := rd.Redirect()
		nt.Redirect(w, r, url, code)
		return nil
	}

	codec := encoding.GetCodec(json.Name) // ignore Accept Header
	data, err := codec.Marshal(v)
	if err != nil {
		return err
	}

	bs, _ := sj.Marshal(NewResponse(data))

	w.Header().Set("Content-Type", ContentType(codec.Name()))
	_, err = w.Write(bs)
	if err != nil {
		return err
	}
	return nil
}

// DefaultErrorEncoder copy from http.DefaultErrorEncoder.
func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
	se := FromError(errors.FromError(err)) // change error to BaseResponse

	codec := encoding.GetCodec(json.Name) // ignore Accept header
	body, err := codec.Marshal(se)
	if err != nil {
		w.WriteHeader(nt.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", ContentType(codec.Name()))
	// w.WriteHeader(int(se.Code)) // ignore http status code
	_, _ = w.Write(body)
}

const (
	baseContentType = "application"
)

// ContentType returns the content-type with base prefix.
func ContentType(subtype string) string {
	return strings.Join([]string{baseContentType, subtype}, "/")
}

func NewResponse(data []byte) BaseResponse {
	return BaseResponse{
		Code:    0,
		Message: "success",
		Data:    sj.RawMessage(data),
	}
}

func FromError(e *errors.Error) *BaseResponse {
	if e == nil {
		return nil
	}
	return &BaseResponse{
		Code:    e.Code,
		Message: e.Message,
	}
}

type BaseResponse struct {
	Code    int32         `json:"code"`
	Message string        `json:"message"`
	Data    sj.RawMessage `json:"data,omitempty"`
}

@JefferyWang
Copy link

自定义MarshalJSON

package cloudapiv3

import (
	"encoding/json"

	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

// Error 实现了
type Error struct {
	Code    string `json:"Code"`
	Message string `json:"Message"`
}

type Response struct {
	RequestId string `json:"RequestId"`
	Error     *Error `json:"Error,omitempty"`
	Data      any    `json:"Data,omitempty"`
}

type CloudAPIV3Response struct {
	Response *Response `json:"Response"`
}

var (
	// MarshalOptions is a configurable JSON format marshaller.
	MarshalOptions = protojson.MarshalOptions{
		EmitUnpopulated: true,
	}
)

// MarshalJSON 序列化
func (resp *Response) MarshalJSON() ([]byte, error) {
	var dataRaw json.RawMessage
	var err error
	switch m := resp.Data.(type) {
	case proto.Message:
		dataRaw, err = MarshalOptions.Marshal(m)
	default:
		dataRaw, err = json.Marshal(m)
	}
	if err != nil {
		return nil, err
	}
	return json.Marshal(&struct {
		RequestId string          `json:"RequestId"`
		Error     *Error          `json:"Error,omitempty"`
		Data      json.RawMessage `json:"Data,omitempty"`
	}{
		RequestId: resp.RequestId,
		Error:     resp.Error,
		Data:      dataRaw,
	})
}

@Qsr9504
Copy link

Qsr9504 commented Feb 17, 2025

主要是在响应阶段,kratos字段如果是默认值,则整体不返回。

如果是状态码不返回,建议使用楼上的方案,制定统一状态码,重写 ResponseEncoder

func encoderResponse() http.ServerOption {
	return http.ResponseEncoder(func(writer nethttp.ResponseWriter, request *nethttp.Request, i interface{}) error {
		reply := &zresp.HttpResponse{
			Code:    200,
			Msg:     "success",
			Data:    i,
			TraceId: request.Header.Get("zoro-traceId"),
		}

		codec := encoding.GetCodec("json")
		data, err := codec.Marshal(reply)
		if err != nil {
			return err
		}
		writer.Header().Set("Content-Type", "application/json")
		_, _ = writer.Write(data)

		return nil
	})
}

但是这样的方案并不能解决 data 内部的空值字段不返回的问题,如果要解决内部,建议 将 proto 生成的 pb 文件进行全文搜索修改,删除 omitempty 信息即可。不过就是每次生成都要修改一遍。所以可以将 protoc 做一个插件,会更加便捷。

@kvii
Copy link
Contributor

kvii commented Feb 17, 2025

但是这样的方案并不能解决 data 内部的空值字段不返回的问题,如果要解决内部,建议 将 proto 生成的 pb 文件进行全文搜索修改,删除 omitempty 信息即可。不过就是每次生成都要修改一遍。所以可以将 protoc 做一个插件,会更加便捷。

@Qsr9504 对于一些需要区分零值和空值的类型,为什么不试试 Well-Known Type 里的 XxValue 呢(文档链接)?在工程的 "third_party/google/protobuf/wrappers.proto" 中已经默认附带了这些 proto 文件,直接引用就好了。就像这样。自己写个工程测一下就知道怎么用了。
不得不吐槽的是 protobuffer 的文档写的是真的抽象。这么点东西都捂着不说明白。

import "google/protobuf/wrappers.proto"; // <- 这样导包

service Greeter {
  rpc Xx (Aa) returns (Bb) {
    option (google.api.http) = {
      post: "/xx",
      body: "*"
    };
  }
}

message Aa {
  google.protobuf.StringValue aaa = 1; // <- 这样使用
}

message Bb {
  google.protobuf.StringValue bbb = 1;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests