えいのうにっき

誤字脱字・記述の誤りなどはこちら https://github.com/a-know/blog.a-know.me へお願いします

中身が不定のJSONオブジェクトをGo言語で扱うのに mattn/go-jsonpointer が便利だった

今日書くのは、先日Go言語の個人プロジェクトである Pixela に手を加えた際に実感したことについて。

先日手を加えたものの一部に、以下のようなものがあった。

> 以下のようなコマンドを実行してみましょう。
>
> curl -X GET https://pixe.la/v1/users/a-know/graphs/stopwatch-test/20200504 -H 'X-USER-TOKEN:thisissecret'
>
> Pixela では、各日付ごとのデータを pixel と呼んでいるのですが、その詳細を取得できるコマンドです。`20200504` のところは詳細を確認したい日付を指定します。このコマンドの実行がうまくいけば、以下のような結果が表示されると思います。
>
> {"quantity":"0.50","optionalData":"{\"stopwatchUsage\":{\"stopwatchUseCount\":2,\"stopwatchPeriods\":[\"20:04:48 - 20:05:00\",\"20:05:13 - 20:05:30\"]}}"}
>
> quantity というのはその日の計測時間の合計値のことです。一方、optionalData に指定されているのは JSON 形式で表されたオブジェクトで、stopwatchUseCount には「計測開始&終了を何セット実施したか」というカウントが、stopwatchPeriods には「計測対象となった時間帯」が、それぞれ記録されています。上記の例だと、「計測開始&終了を2セット」「計測したのは 20時04分48秒〜20時05分00秒 と 20時05分13〜20時05分30秒」という情報がセットされている形になります。

Pixel という以下のような(とはいえ、説明に不要なものは省略している。)エンティティがあって、

type Pixel struct {
    Date              string        `json:"date"`
    Quantity          string        `json:"quantity"`
    OptionalData      string        `json:"optionalData"`
}

このうち OptionalData は、ユーザーが自由にJSON文字列を格納することができる、という仕様。そんな仕様だったところに、今回のアップデートで、システム側も OptionalData を用いるようにしたくなった、ということになる。

ざっくり要件をまとめるとすると、以下のような感じだろうか。

  • OptionalData を Unmarshal したものに、"stopwatchUsage":{"stopwatchUseCount":1,"stopwatchPeriods":["20:00:00 - 20:05:00"]} といったオブジェクトを突っ込みたい。
  • OptionalData にもともと格納されていた情報は、もちろん維持しなければならない。
    • どんな情報が格納されているかはわからない。保証されていることは、それがJSON形式の文字列である、ということだけ。
    • キーが stopwatchUsage と重複するケースは考慮しない(システム側が上書きすることを許容する)

最初に行き着いた情報はこちら golang は ゆるふわに JSON を扱えまぁす! — KaoriYa 。ただ、ここにある方法だと、全くの未知の構造のオブジェクトに対して適用することができないように思われた(これはこれで便利に使える場面はあるものだと感じたけれど)。

ただ、上記のページ内で紹介されていた mattn/go-jsonpointer が、まさに今回のケースでぴったりハマりそうなもので、実際、これを使うことで非常にすんなり実装を終えることができた。

github.com

JSON Pointer の GO実装。以下に、今回のケースにおいてこれを用いた実例を示す。

以下のように、OptionalData に突っ込みたい struct を予め用意しておき、

count := 1
periods := []string{"20:00:00 - 20:05:00"}
usage := &stopwatchUsage{
    StopwatchUseCount: count,
    StopwatchPeriods:  periods,
}

*interface{} を渡して OptionalData を Unmarshal。

var obj interface{}
json.Unmarshal([]byte(pixel.OptionalData), &obj)

あとは、Unmarshal されたものに対して go-jsonpointer を使って、stopwatchUsage というキーで set してやるだけでよかった。

jsonpointer.Set(obj, "/stopwatchUsage", usage)
optinalDataString, err := json.Marshal(obj)

/stopwatchUsage というのが JSON Pointer(RFC6901 として標準化された、JSONオブジェクトに対するクエリ文字列)。

他にも、Get Remove ができる。リポジトリの README に非常に簡潔に書いていただいていたので、迷うことも何もなかった。「OptionalData 、なんでそんな自由な項目にしたんや......ワイのバカ!」なんて後悔することもなくて、本当に良かった(反省はしたほうがよさそう)。感謝です。

関連