gRPCのシリアライゼーション形式をJSONにする

gRPCで送受信されるメッセージは、標準ではProtocol Buffersでシリアライゼーションされることになっている。一方、gRPCの[wire protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md)はそこは柔軟になっていて、実際、多くの実装ではシリアライゼーション形式をカスタマイズ可能だ。

たとえば、ある種のニーズのためにFlatBuffersが必要だとか、HTTP/2をサポートする標準的なツールでの解析のためにJSONのほうが可読性が良いとか、社内のバイナリ表現の一貫性のためMsgPackが必要だとか、宗教的な理由でProtobufを使えないとか、そういうときはスキーマだけprotobufで書いておいて、シリアライズは好きなようにやれば良い。

で、具体的にはそれはどうやったらできるのだろう。これが本稿の話題である。いくつかの言語で実際にJSONでシリアライズするクライアントとサーバーを[書いてみた](https://github.com/yugui/grpc-custom-serializer)ので、その結果を紹介する。各言語の特色が良く出ていて面白かった。


|言語  |JSON変換ユーティリティ  |カスタマイズ単位　  | カスタマイズ方法 |
|-----|-------------------|---------------|--------------|
|C++  |[`google/protobuf/util/json_util.h`](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.util.json_util) | メッセージクラス毎 | [`SerializationTraits`](https://grpc.io/grpc/cpp/classgrpc_1_1_serialization_traits.html)を特殊化 |
|Go   |[`github.com/golang/protobuf/jsonpb`](godoc.org/github.com/golang/protobuf/jsonpb)|呼び出し毎 / クライアントインスタンス毎| [`CallOption`](https://godoc.org/google.golang.org/grpc#CallContentSubtype)を渡す |
|Ruby |[`.encode_json`, `.decode_json`](https://developers.google.com/protocol-buffers/docs/reference/ruby-generated#message) | メソッド定義毎 | `RpcDesc`のインスタンスをメタプログラミングで書き換え |
|Java |[`io.grpc.protobuf.ProtoUtils.jsonMarshaller`](https://grpc.io/grpc-java/javadoc/io/grpc/protobuf/ProtoUtils.html#jsonMarshaller-T-)|メソッド定義毎|カスタムの[`MethodDescriptor`](https://grpc.io/grpc-java/javadoc/io/grpc/MethodDescriptor.html)インスタンスを作り、それを用いてサーバー/クライアントのメソッドを再実装/オーバーライド|
|Node |シリアライズはできるが、デシリアライズAPIがない! | あとで書く |あとで書く|

例としては、次のようなサービスを各言語で実装しJSONでシリアライズしている。

```proto:greeter.proto
syntax = "proto3";
package greeter;

option go_package = "greeterproto";
option java_package = "com.github.yugui.grpc_custom_serializer";

message RequestProto {
        string name = 1;
}

message ResponseProto {
        string message = 1;
}

service Greeter {
        rpc Greet(RequestProto) returns (ResponseProto);
}
```

なお、protobufのランタイムライブラリは標準でprotobuf形式やJSONへのシリアライゼーションはサポートしているため、`struct`や`class`とJSONのマッピングを自作する必要はない。一方、実際に他の形式を使いたい場合はそのサポートを得られないので、自分で何らかのアダプターを書かねばならないだろう。

# C++
C++のシリアライズ方式カスタマイズは、ある意味とても「らしい」造りをしている。

* ProtobufメッセージのクラスとJSONの間のシリアライズ/デシリアライズには[`google/protobuf/util/json_util.h`](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.util.json_util)の関数を使う。
* ランタイムが特定のprotobufメッセージ型をシリアライズする方式をカスタマイズするには、Traitクラスを特殊化する

```c++:serialization.h
#include <grpc++/impl/codegen/config_protobuf.h>
#include <grpc++/impl/codegen/proto_utils.h>

namespace greeter {
class RequestProto;
class ResponseProto;
}  // namespace greeter

namespace grpc {
template <>
class SerializationTraits<greeter::RequestProto> {
 public:
  static Status Serialize(const grpc::protobuf::Message& msg,
                          grpc_byte_buffer** bp, bool* own_buffer);
  static Status Deserialize(grpc_byte_buffer* buffer,
                            grpc::protobuf::Message* msg);
};

// (引用註:ResponseProto用特殊化は略)
...
}  // namespace grpc
```

見て分かるように、[`grpc::SerializationTraits`](https://grpc.io/grpc/cpp/classgrpc_1_1_serialization_traits.html)をメッセージクラス`RequestProto`向けに特殊化すれば、サーバーは`RequestProto`をシリアライズ/デシリアライズするときにその`static`メンバー関数を呼び出す。

で、そのメンバー関数のほうは`grpc::protobuf::Message&`と`grpc_byte_buffer`の間をうまく変換するように実装すれば良いことになっている。実装に目を移すと、

```c++:serialization.cc
...

namespace {
grpc::Status GenericJSONSerialize(const grpc::protobuf::Message& msg,
                                  grpc_byte_buffer** bp, bool* own_buffer) {
  std::string buf;
  const auto status = google::protobuf::util::MessageToJsonString(msg, &buf);
  if (!status.ok()) {
    std::cerr << "Failed to serialize message: " << status.error_code() << ":"
              << status.error_message() << std::endl;
    return grpc::Status(grpc::StatusCode::INTERNAL,
                        "Failed to serialize message");
  }

  auto buf_slice = grpc::SliceFromCopiedString(buf);
  *bp = ::grpc_raw_byte_buffer_create(&buf_slice, 1);
  grpc_slice_unref(buf_slice);
  *own_buffer = true;

  return grpc::Status::OK;
}


grpc::Status GenericJSONDeserialize(grpc_byte_buffer* buffer,
                                    grpc::protobuf::Message* msg) {
  // (引用註:Deserializeも似た感じなので略)
  ...
}
}  // namespace

namespace grpc {
using greeter::RequestProto;
using greeter::ResponseProto;

Status SerializationTraits<RequestProto>::Serialize(
    const grpc::protobuf::Message& msg, grpc_byte_buffer** bp,
    bool* own_buffer) {
  return GenericJSONSerialize(msg, bp, own_buffer);
}

Status SerializationTraits<RequestProto>::Deserialize(
    grpc_byte_buffer* buffer, grpc::protobuf::Message* msg) {
  return GenericJSONDeserialize(buffer, msg);
}

// (引用註:ResponseProto用特殊化は略)
...
}  // namespace grpc
```

なんか、APIのお約束の関係で多少複雑であるものの、基本的には`google::protobuf::util::MessageToJsonString`(デシリアライズなら`JsonStringToMessage`)を呼び出しているだけだ。

# Go
Goも非常に「らしい」造りである。

* ProtobufメッセージをJSONにシリアライズ(あるいはデシリアライズ)するには[`github.com/golang/protobuf/jsonpb`パッケージ](https://godoc.org/github.com/golang/protobuf/jsonpb)を使う。
* 新しいシリアライズ方式をサーバーに使わせるには、[`"google.golang.org/grpc/encoding".Codec`インターフェース](https://godoc.org/google.golang.org/grpc/encoding#Codec)を実装し、登録する。
* サーバーはgRPC wire protocolの`Content-Type`や`Accept`ヘッダを見て適切なCodecを選択する。
* クライアントはリモートメソッド呼び出しの都度[`CallOption`](https://godoc.org/google.golang.org/grpc#CallContentSubtype)で、今回の通信で使うシリアライズ形式を指定できる。
  * 他の`CallOption`と同じく、`Dial`時に[`grpc.WithDefaultCallOptions`](https://godoc.org/google.golang.org/grpc#WithDefaultCallOptions)を渡してデフォルトのシリアライズ方式を指定しておくこともできる。

```go:encoding/json.go
// Package encoding defines the JSON codec.
package encoding

import (
	"bytes"
	"fmt"

	"github.com/golang/protobuf/jsonpb"
	"github.com/golang/protobuf/proto"
	"google.golang.org/grpc/encoding"
)

const (
	// Name is the name registered for the json encoder.
	Name = "json"
)

// codec implements encoding.Codec to encode messages into JSON.
type codec struct {
	m jsonpb.Marshaler
	u jsonpb.Unmarshaler
}

func (c *codec) Marshal(v interface{}) ([]byte, error) {
	msg, ok := v.(proto.Message)
	if !ok {
		return nil, fmt.Errorf("not a proto message but %T: %v", v, v)
	}

	var w bytes.Buffer
	if err := c.m.Marshal(&w, msg); err != nil {
		return nil, err
	}
	return w.Bytes(), nil
}

func (c *codec) Unmarshal(data []byte, v interface{}) error {
	msg, ok := v.(proto.Message)
	if !ok {
		return fmt.Errorf("not a proto message but %T: %v", v, v)
	}
	return c.u.Unmarshal(bytes.NewReader(data), msg)
}

// Name returns the identifier of the codec.
func (c *codec) Name() string {
	return Name
}

func init() {
	encoding.RegisterCodec(new(codec))
}
```
(引用にあたり、ログ出力とコメントを少し削った)

`Codec`の実装は基本的に`jsonpb`が提供するmarshaler/unmarshalerのアダプタに過ぎない。最後に、`encoding.RegisterCodec`を使って自作した`Codec`を登録している。

`codec.Name()`は`"json"`を返すので、ここで登録した`Codec`は要求されたMIME typeが`application/grpc+json`の場合に利用されることになる。
では、`Content-Type`や`Accept`が`application/grpc+json`になるようにクライアントを設定するにはどうすれば良いのか。それが下記である。

```go:client/main.go
func greet(ctx context.Context, conn *grpc.ClientConn, name string) (string, error) 
...
func run(ctx context.Context) error {
	opts := []grpc.DialOption{
		grpc.WithInsecure(),
		grpc.WithDefaultCallOptions(grpc.CallContentSubtype(encoding.Name)),
	}
	conn, err := grpc.DialContext(ctx, *addr, opts...)
	if err != nil {
		glog.Errorf("Failed to connect to the server %s: %v", *addr, err)
		return err
	}
	defer conn.Close()

	msg, err := greet(ctx, conn, *name)
	if err != nil {
		glog.Errorf("Failed to call greeter: %v", err)
		return err
	}
	_, err = fmt.Fprintln(os.Stdout, msg)
	return err
}

...
```

クライアントスタブのメソッドを呼び出すときに`grpc.callContentSubtype("json")`を渡すか、または`Dial`するときにデフォルト設定をしておけば良い。ここでは後者の方式で、`DialOption`として指定している。

# Ruby
ここまで見てきたようにC++でもGoでもシリアライズ形式はカスタマイズ出来る仕組みが基本にあって、デフォルト設定としてprotobuf形式をインストールしてあるわけだが、Rubyも同じである。Rubyのそれはメタプログラミングでできている。

* `.proto`から生成されたメッセージごとのクラスは`encode_json`, `decode_json`のようなクラスメソッドを持っている。これをサーバー/クライアントのランタイムに使わせれば良い。
  * https://developers.google.com/protocol-buffers/docs/reference/ruby-generated#message
* `.proto`から生成されたサーバー/クライアントのスタブライブラリは、[GRPC::Generic::Dsl](http://www.rubydoc.info/gems/grpc/GRPC/GenericService/Dsl)モジュールが提供するDSLを使って、それぞれのgRPCメソッドごとに[`GRPC::RpcDesc`](http://www.rubydoc.info/gems/grpc/GRPC/RpcDesc)のインスタンスを生成する。
　　* 各`GRPC::RpcDesc`インスタンスの`marshal_method`, `unmarshal_method`というattributeに、マーシャル時に呼び出すメソッド名が設定されている。これを書き換えれば良い。

もう少し詳しく見ていきたい。生成されたスタブはこんな感じである。`self.marshal_class_method`とか`self.unmarshal_class_method`の設定に従い、`rpc :Greet, RequestProto, ResponseProto`という呼び出しの中で`RpcDesc`のインスタンスが生成されて`Greeter::Greeter::Service`に登録されている。

```ruby:lib/greeter/greeter_services_pb.rb
* # Generated by the protocol buffer compiler.  DO NOT EDIT!
# Source: greeter/greeter.proto for package 'greeter'

require 'grpc'
require 'greeter/greeter_pb'

module Greeter
  module Greeter
    class Service

      include GRPC::GenericService

      self.marshal_class_method = :encode
      self.unmarshal_class_method = :decode
      self.service_name = 'greeter.Greeter'

      rpc :Greet, RequestProto, ResponseProto
    end

    Stub = Service.rpc_stub_class
  end
end
```

この自動生成コードを手で書き換えて`self.marshal_class_method = :encode_json`とかしてしまえば話は早いが、メンテナンス性が最悪(`.proto`ファイルを修正してコードが再生成されるたびにまた手で書き換えるのは嫌だ)なので他の方式が必要だ。

そこで、こんなライブラリを用意した。

```ruby:lib/greeter/json_serialization.rb
require 'grpc'

module GRPC::GenericService::Dsl
  def use_json_marshaler(*method_names)
    targets = rpc_descs
    unless method_names.empty?
      targets = targets.select {|name,| method_names.include?(name)}
    end
    targets.each do |name, desc|
      desc.marshal_method = :encode_json
      desc.unmarshal_method = :decode_json
    end
    self
  end
end
```

このライブラリは`GRPC::GenericService::Dsl`にモンキーパッチを当てて`use_json_marshaler`というメソッドを生やす。`use_json_marshaler`は、レシーバーであるクラスに設定済みの`RpcDesc`インスタンスを探索し、対象となるインスタンスの`marshal_method`, `unmarshal_method`　attributesを書き換える。

下記のように対象となるgRPCメソッド名を`use_json_marshaler`に渡せば、特定のメソッドのみJSON利用設定をする。

```ruby
Greeter::Greeter::Service.use_json_marshaler :Greet
```

一方、メソッド名を渡さずに`use_json_marshaler`を呼び出すとレシーバ内の全gRPCメソッドにJSON利用設定をする。

# Java
JavaもJavaらしいつくりのコードだったと思う。つまり、Java版ランタイムも基本的にRubyと同じ構造になっているのだが、メタプログラミングとか無しで自分で(あるいはIDEが)ちまちまコードを書く方式になっているという意味で。

* protoメッセージとJSONの間のシリアライズ/デシリアライズには[`io.grpc.protobuf.ProtoUtils.jsonMarshaller`メソッド](https://grpc.io/grpc-java/javadoc/io/grpc/protobuf/ProtoUtils.html#jsonMarshaller-T-)が返すインスタンスを使う。
* 各gRPCメソッドごとに[`io.grpc.MethodDescriptor`クラス](https://grpc.io/grpc-java/javadoc/io/grpc/MethodDescriptor.html)のインスタンスが生成されている。これを元にして、`ProtoUtils.jsonMarshaler`を使うバージョンをメソッドごとに生成し直す
* クライアントスタブとサーバースタブで`MethodDescriptor`を参照している部分をオーバーライドして、自作のバージョンを使うようにする。

Rubyだとこの辺は全部メタプログラミングの背後に隠れたんだが。

まず、`MethodDescriptor`のカスタマイズから見ていこう。`.proto`の`rpc Greet(RequestProto) returns(ResponseProto)`に対応して次のようなコードが生成されている。

```java:target/generated-sources/protobuf/grpc-java/com/github/yugui/grpc_custom_serializer/GreeterGrpc.java
public final class GreeterGrpc {
  ... // (中略)

  public static final io.grpc.MethodDescriptor<com.github.yugui.grpc_custom_serializer.GreeterOuterClass.RequestProto,
      com.github.yugui.grpc_custom_serializer.GreeterOuterClass.ResponseProto> METHOD_GREET = getGreetMethod(); 

  ...  // (中略)
  
  @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/1901")
  public static io.grpc.MethodDescriptor<com.github.yugui.grpc_custom_serializer.GreeterOuterClass.RequestProto,
      com.github.yugui.grpc_custom_serializer.GreeterOuterClass.ResponseProto> getGreetMethod() {    
    io.grpc.MethodDescriptor<com.github.yugui.grpc_custom_serializer.GreeterOuterClass.RequestProto, com.github.yugui.grpc_custom_serializer.GreeterOuterClass.ResponseProto> getGreetMet
hod;                                                     
     ... // (中略)                                                        
     return getGreetMethod;                                  
  }                                                                                                  
  ... // (中略)
}
```

これをカスタマイズしたバージョンを作れば良いので、次のようなクラスを作った。

```java:src/main/java/com/github/yugui/grpc_custom_serializer/GreeterDescriptions.java
package com.github.yugui.grpc_custom_serializer;

import com.github.yugui.grpc_custom_serializer.GreeterOuterClass.RequestProto;
import com.github.yugui.grpc_custom_serializer.GreeterOuterClass.ResponseProto;
import io.grpc.MethodDescriptor;
import io.grpc.protobuf.ProtoUtils;

final class GreeterDescriptions {
  static final MethodDescriptor<RequestProto, ResponseProto> METHOD_GREET =
      GreeterGrpc.METHOD_GREET
          .toBuilder(
              ProtoUtils.jsonMarshaller(RequestProto.getDefaultInstance()),
              ProtoUtils.jsonMarshaller(ResponseProto.getDefaultInstance()))
          .build();
}
```

あとは、クライアントstubやサーバーstubのメソッドをオーバーライドして`target/generated-sources/protobuf/grpc-java/com/github/yugui/grpc_custom_serializer/GreeterGrpc.java`内の`METHOD_GREET`の代わりに`GreeterDescriptions.METHOD_GREET`を喰わせれば良いのだが、

```java:src/main/java/com/github/yugui/grpc_custom_serializer/GreeterServer.java
import static com.github.yugui.grpc_custom_serializer.GreeterDescriptions.METHOD_GREET;

... // (中略)

public class GreeterServer {                           
   ... // (中略)
   public static class GreeterImpl implements BindableService {
    public ServerServiceDefinition bindService() {
      final ServerCallHandler<RequestProto, ResponseProto> greet =
          asyncUnaryCall(                                                      
              new UnaryMethod<RequestProto, ResponseProto>() {   
                @Override                            
                public void invoke(       
                    RequestProto request, StreamObserver<ResponseProto> responseObserver) {
                  GreeterImpl.this.greet(request, responseObserver);
                }                                    
              });       
      return ServerServiceDefinition.builder(GreeterGrpc.getServiceDescriptor().getName())
          .addMethod(METHOD_GREET, greet)                        
          .build();                        
    }                                 
   ... // (中略)
}
```

辛い。仮にrpcメソッドが10個あったとしたら、これを10回書くのである。辛い。

クライアントのほうは別の辛さがある。　

```java:src/main/java/com/github/yugui/grpc_custom_serializer/GreeterClient.java
import static com.github.yugui.grpc_custom_serializer.GreeterDescriptions.METHOD_GREET;

... // (中略)

 private static final class GreeterStub extends AbstractStub<GreeterStub> {
    ... // (中略)
                                       
    ResponseProto greet(RequestProto request) {
      return blockingUnaryCall(getChannel(), METHOD_GREET, getCallOptions(), request);
    }
  }
```

自動生成されるコードの中のクライアントのスタブクラスは`final`なので、ほぼ同じコードを自分で書き直すことにより、カスタムの`MethodDescriptor`を`blockingUnaryCall`に渡す。

# まとめ


|言語  |JSON変換ユーティリティ  |カスタマイズ単位　  | カスタマイズ方法 |
|-----|-------------------|---------------|--------------|
|C++  |[`google/protobuf/util/json_util.h`](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.util.json_util) | メッセージクラス毎 | [`SerializationTraits`](https://grpc.io/grpc/cpp/classgrpc_1_1_serialization_traits.html)を特殊化 |
|Go   |[`github.com/golang/protobuf/jsonpb`](godoc.org/github.com/golang/protobuf/jsonpb)|呼び出し毎 / クライアントインスタンス毎| [`CallOption`](https://godoc.org/google.golang.org/grpc#CallContentSubtype)を渡す |
|Ruby |[`.encode_json`, `.decode_json`](https://developers.google.com/protocol-buffers/docs/reference/ruby-generated#message) | メソッド定義毎 | `RpcDesc`のインスタンスをメタプログラミングで書き換え |
|Java |[`io.grpc.protobuf.ProtoUtils.jsonMarshaller`](https://grpc.io/grpc-java/javadoc/io/grpc/protobuf/ProtoUtils.html#jsonMarshaller-T-)|メソッド定義毎|カスタムの[`MethodDescriptor`](https://grpc.io/grpc-java/javadoc/io/grpc/MethodDescriptor.html)インスタンスを作り、それを用いてサーバー/クライアントのメソッドを再実装/オーバーライド|
|Node |シリアライズはできるが、デシリアライズAPIがない! | あとで書く |あとで書く|

ところで、go版のサーバーは`Content-Type`を見て`Codec`を使い分けるわけだが、他の言語のクライアントで`Content-Type`を設定するAPIは無く、常に`Content-Type: application/grpc+proto`が付くように見える。従って、go版のサーバーはシリアライズ方式をカスタマイズした他の言語のクライアントとは互換性がない。