配列型のJSONから gRPC(protobuf) 用のモックデータを読み込む
gRPC のハンズオンがてら適当なサーバを書いてみて、ある程度ハンドラが充実してきたので今度は適当なデータをJSONで用意して読み込んでサーバで返すようにしよう、としたときに意外と大変だったという記録です。
動作するサーバコードは以下のリポジトリに置いてあります。
色々試したところそもそもやっていることの筋が悪いような気がしたのですが、一応動いたは動いたので記録として残しておきます。
サーバ・データ定義
サービス定義
サービス定義は以下のようなよくある ToDo アプリです。
syntax = "proto3"; import "google/protobuf/timestamp.proto"; service ReadOnlyTodo { rpc GetTodos(GetTodosRequest) returns (GetTodosResponse) {} } message Todo { int32 id = 1; string title = 2; google.protobuf.Timestamp deadline = 3; } message GetTodosRequest { } message GetTodosResponse { repeated Todo todos = 1; }
サーバ定義
サーバ定義は以下のようにしています。
type todoServer struct { todos []*pb.Todo mu sync.Mutex }
今回はこの todos
にJSONのデータをロードするための関数を生やそうと試行錯誤しています。ロードするJSONは以下のようなものです。
[ { "id": 1, "title": "First ToDo", "deadline": "2018-09-01T12:34:56.789Z" }, { "id": 2, "title": "Second ToDo", "deadline": "2018-09-01T12:34:56.789Z" } ]
配列形式で複数のエントリが表されています。[ptypes]
試したパターン
実際に試した順番からはちょうど逆転しますが成功したパターンからご紹介します。
成功したパターン
まずは成功したパターンです。
golang/protobuf#675 のコメントを参考にして書きました。
普段の json.Unmarshal
と比較するととても複雑なことをしているように見えます。
func (s *todoServer) loadTodos() { jsonBytes, _ := ioutil.ReadFile(dataFile) jsonString := string(jsonBytes) jsonDecoder := json.NewDecoder(strings.NewReader(jsonString)) // read open bracket if _, err := jsonDecoder.Token(); err != nil { log.Fatal(err) } for jsonDecoder.More() { todo := pb.Todo{} if err := jsonpb.UnmarshalNext(jsonDecoder, &todo); err != nil { log.Fatal(err) } s.todos = append(s.todos, &todo) } }
Unmarshal に標準の encoding/json ではなく protobuf/jsonpb というパッケージを利用しています。 叩いたところ無事データが返ってきました。
$ grpc_cli call localhost:10000 GetTodos "" connecting to localhost:10000 todos { id: 1 title: "First ToDo" deadline { seconds: 1535805296 nanos: 789000000 } } todos { id: 2 title: "Second ToDo" deadline { seconds: 1535805296 nanos: 789000000 } }
直感的に jsonpb を使った場合
いちいち UnmarshalNext
などせずに配列型を直接データの格納先に指定してやればいいのではと思い試してみました。
func (s *todoServer) loadTodos_byInstinctiveJSONPBUsage() { reader, _ := os.Open(dataFile) if err := jsonpb.Unmarshal(reader, &s.todos); err != nil { log.Fatal(err) } }
これを実行すると...というよりコンパイルの時点でエラーが出て
*[]*todo.Todo does not implement "github.com/golang/protobuf/proto".Message (missing ProtoMessage method)
と言われます。つまり pb.Todo
は Message
の interface を満たしているけれど []pb.Todo
には ProtoMessage
のメソッドが存在しないので jsonpb.Unmarshal
のシグネチャ違反ですよと言われているんですね。
標準の encoding/json パッケージを利用した場合
そもそも jsonpb を利用せずに json を利用したらどうなるかです。
func (s *todoServer) loadTodos_byStandardJSONPackage() { jsonBytes, _ := ioutil.ReadFile(dataFile) if err := json.Unmarshal(jsonBytes, &s.todos); err != nil { log.Fatal(err) } }
これを実行すると...
json: cannot unmarshal string into Go struct field Todo.deadline of type timestamp.Timestamp
と返ってきます。string
型を timestamp.Timestamp
型に Unmarshal できないと言っていますね。timestamp.Timestamp
型は Protocol Buffer の ptype (もしくは well-known type) です。
Go 自体そこまで詳しくはないのですが、Unmarshal の振る舞いを変えられたら json でもなんとかなる...んでしょうか。