Protocol BuffersをJSONに変換する

React Native + GoのProobuf API Serverの構成でアプリケーションを作っているのですが、次の条件でdeserializeができないことがわかってしまった。

decodeIO/protobuf.js の方も一応試してみたけど、うまく動かないので急遽Protobufの使用を諦めてjsonに切り替えることにします。 こちらも詳細は↓にまとめました。あとでGithubの方にもIssue建てようかと思います

protobuf/jsonpbJSONに変換する

開発期間も短いので今からサーバの実装を変えていくのはできればさけたい。 モックサーバとかはすでにProtobufで書いていたので、それをJSONに変換する処理を書いていく方法を検討してみた。

今回使っている型はそんなに多くないので自前でさっと書けないかなとか考えつつ調べてみたら、protobufの中に jsonpb という便利そうなモジュールがあった。 ProtobufのモックAPIJSONAPIに書き換えてみる。

func confirmProfile(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)

    p := &pb.ConfirmProfileResponse{
        HasProfile: true,
        Profile: &pb.ProfileResponse{
            Uid: "uid0001",
            Username: "c_bata_",
            IconUrl: "http://example.com/hoge.png",
            WebsiteUrl: "http://example.com",
            CreatedAt: 1000,
            UpdatedAt: 1000,
        },
    }

    m := jsonpb.Marshaler{
        EmitDefaults: true,
        Indent: "    ",
        OrigName: true,
    }
    m.Marshal(w, p)
    return
}

curlで確認。

$ curl localhost:8080 
{
    "has_profile": true,
    "profile": {
        "uid": "uid0001",
        "username": "c_bata_",
        "icon_url": "http://example.com/hoge.png",
        "website_url": "http://example.com",
        "created_at": "1000",
        "updated_at": "1000"
    }
}

実装はreflectを多用することになるので、パフォーマンスには課題があるかもしれませんが簡単にJSONAPIに変換できた。 ただ、数値(int64)で欲しかったものがstringになってしまっていて、整数値として出力してほしい。 もしかするとバグかなと思い、ResponseWriterの代わりに次のLoggerBufferを仕込みつつ原因となっている行を探してみた。

type LoggerBuffer struct {
    bytes.Buffer
}

func (b *LoggerBuffer) Write(p []byte) (n int, err error) {
    log.Printf("LoggerBuffer: %s\n", p)
    return b.Buffer.Write(p)
}

次の行が原因らしく、どうやらint64とuint64の型の場合はあえて、ダブルクオーテーションで囲っている。 こうしている理由は、JavaScriptの仕様に何か関係しているのかもしれないと思い調べてみた。

https://github.com/golang/protobuf/blob/master/jsonpb/jsonpb.go#L504-L511

EcmaScriptのNumber型

調べてみると次のStackoverflowの回答が見つかった。

math - What is JavaScript's highest integer value that a Number can go to without losing precision? - Stack Overflow

Number型で表現可能なのは +/- 253 の範囲らしい。 あとbit演算は32bitの範囲で利用できる。 実際にChromeのConsoleで挙動をみてみた。

> Math.pow(2, 53)
9007199254740992
> Math.pow(2, 64)
18446744073709552000
> Math.pow(2, 53) - 1
9007199254740991
> Math.pow(2, 53) + 1
9007199254740992

値としては持つことができているが、253を超える範囲では数値演算が正しく動作しないことが確認できる。 負の数も同様。

> - Math.pow(2, 53)
-9007199254740992
> - Math.pow(2, 53) -1
-9007199254740992
> - Math.pow(2, 53) + 1
-9007199254740991

そもそも何故 int64を使ったのか

GoのAPIサーバとJS間で、文字列よりも言語間で統一されてそうというイメージで時刻はUnixTimeで表現していた。 golangでは time.Time 型に Unix() メソッドが提供されている。 このメソッドは int64 を返していたので、その値をProtocol Buffersでも使用していた。

https://godoc.org/time#Time.Unix

ただし、先程のEcmaScriptのNumber型の仕様上、JSでint64を扱うのはやめておいたほうがよさそう。 関連する問題に、「2038年問題」というのがあるみたい。

2038年問題 - Wikipedia

こういった問題がある以上、API上での時間の扱いとしてUnixTimeを使うのはあまりいい方法ように思えてきた。

ISO8601とRFC3339

ISO8601形式がよさそう。文字列なので先程のような問題は解決できる。 今後はこれを使おうかと思う。

golangtime.Time をISO8601形式に変換する

time.Now().UTC().Format(time.RFC3339)

あとは、go-sql-driver/mysqlのlocの設定もつけておいた ( ?parseTime=true&loc=Local )。

おわりに

常識だったのかもしれない。この機会に知ることができてよかった。

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)