きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

配列型の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
}

今回はこの todosJSONのデータをロードするための関数を生やそうと試行錯誤しています。ロードする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.TodoMessage の 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 でもなんとかなる...んでしょうか。