JSONをGolangのstructに変換する

2017-07-19

先日のgolang.tokyo #7で自社パッケージの紹介LT大会にてLTでjson2structと言うパッケージの紹介をさせていただきました。

細かい使い方や既存の同様のツールとの差異について書いていきたいと思います。

ツールの概要

GolangではJSONからデータを取得する際にあらかじめstructの定義をしておくことがありますが、その際に下記のようにタグ等など設定しなくてはならず、手間がかかってしまうのが面倒くさい。 また外部のAPI等でJSONの形式が複雑な場合その定義はより手間がかかってしまう。

{"url": "http://blog.yudppp.com", "text": "Hello:)", "status": 1}
type Blog struct {
    Status int    `json:"status"`
    Text   string `json:"text"`
    URL    string `json:"url"`
}

それらを解決するために、JSONからGolangのstructの形式に変換するツールは幾つか既に存在しています・。

私はこれらのツールを使っていたのですが、実際に使う際に使いにくいポイントがあったので自分で作成しました。

json-to-go

json-to-goはサイト上でJSONをstructに変換してくれるツールです。

https://mholt.github.io/json-to-go/

こちらの実行結果を見ていくと普段自分が書かないstructのnestや[]structといったものができてしまい、個人的には実用的でないと判断し使うことをやめました。

またJavaScript製でgofmtがされていないものが出力されます。

input

{"url": "http://blog.yudppp.com", "text": "Hello:)", "status": 1, "categories": [{"name": "golang"}]}

output

type AutoGenerated struct {
    URL string `json:"url"`
    Text string `json:"text"`
    Status int `json:"status"`
    Categories []struct {
        Name string `json:"name"`
    } `json:"categories"`
}

gojson

https://github.com/ChimeraCoder/gojson

gojsonも同様のツールでcliを用いて、JSONのinputに対してstructを吐き出します。defaultではstructがnestするのですが、-subStructのオプションを設定することによって別のstructに分けることができます。

ただし生成されるサブストラクト名がBlog_sub1という名前でこのまま使いたくなくrenameする必要がありました。

$ echo '{"url": "http://blog.yudppp.com", "text": "Hello:)", "status": 1, "categories": [{"name": "golang"}]}' \
| gojson -name Blog
package main

type Blog struct {
    Categories []struct {
        Name string `json:"name"`
    } `json:"categories"`
    Status int64  `json:"status"`
    Text   string `json:"text"`
    URL    string `json:"url"`
}
$ echo '{"url": "http://blog.yudppp.com", "text": "Hello:)", "status": 1, "categories": [{"name": "golang"}]}' \
| gojson -name Blog -subStruct
package main

type Blog struct {
    Categories []Blog_sub1 `json:"categories"`
    Status     int64       `json:"status"`
    Text       string      `json:"text"`
    URL        string      `json:"url"`
}

type Blog_sub1 struct {
    Name string `json:"name"`
}

またWeb上で確認できるものがなく、CLIで確認する方法しかありませんでした。

json2struct

これらを受けてjson2structを作成しました。

まずCLIWebの2種類の方法で扱うことができます。

json2structを使うと先ほどのJSONが下記のようになります

$ echo '{"url": "http://blog.yudppp.com", "text": "Hello:)", "status": 1, "categories": [{"name": "golang"}]}' \
| json2struct -name Blog
type Blog struct {
    Categories []BlogCategory `json:"categories"`
    Status     int            `json:"status"`
    Text       string         `json:"text"`
    URL        string         `json:"url"`
}

type BlogCategory struct {
    Name string `json:"name"`
}

gojsonではBlog_sub1だったものがBlogCategoryに変わっています。 こちらでポイントなのがプロパティ名がCategoriesと複数形だったものを配列だったので単数形にCategoryと変換したものともとのstruct名であるBlogと足しあわせてBlogCategoryとしています。(外部のパッケージ使っただけですが)

またサブストラクトの命名をオプションで色々変更ができるようにしていて、元のstruct名を引き継ぐのが冗長的で嫌いという人には-shortのOptionをつけるとCategoryというstruct名になったり、suffixにResponseをつけたいというのであれば-suffix ResponseとするとそれぞれBlogResponse,BlogCategoryResponseとなるようになります。

細かなOptionについては下記のようになっています。 最新はREADMEを参照してください。

option description
name メインのストラクト名を指定する
prefix ストラクトのprefixを指定する
suffix ストラクトのsuffixを指定する
short サブストラクトの命名を簡易にする
local local変数にする
omitempty omitemptyのオプションをつける
example example tagをつける(後述します)

欲しいオプション等ありましたら気軽に実装していきたいと思いますのでissueとかに書いていただけたらと思います。

playgroundについて

こちらで簡単にWeb上でjson2structを利用することができます。 またオプションの値をWeb上で変えられるので出力結果を見ながらオプションの変更をすることができます。

技術的にはGopherJSvectyを使っています。 GopherJSはGoで書かれたコードをJavaScriptに変換してくれるもので、vectyは語弊があるかもしれませんが簡単に言うとGopherJSをReact風にかけるもので、これにより簡単にGopherJSを導入することができました。

example tagについて

-exampleをつけるとexampleタグが自動でつくようになります。 このexampleタグを利用するとJSONのサンプルの値を簡単に入れれるようになります。(完全に元のJSONに戻るもので現状ありません(配列の扱いが辛い))

package main
import (
    "fmt"

    "github.com/yudppp/structs"
)

type Blog struct {
    Categories []BlogCategory `json:"categories"`
    Status     int            `json:"status" example:"1"`
    Text       string         `json:"text" example:"Hello:)"`
    URL        string         `json:"url" example:"http://blog.yudppp.com"`
}

type BlogCategory struct {
    Name string `json:"name" example:"golang"`
}

func main() {
    blog := structs.NewExample(Blog{}).(Blog)
    fmt.Println(blog.Text) # -> Hello:)
    fmt.Println(blog.Categories[0].Name) # -> golang
}

このように簡単にサンプル用の値を入れることができるのでテストや開発時のモックAPI、ドキュメント作成時に利用していければと思っています。

まとめ

ストラクトを分けたくてストラクト名をいい感じにしたいならjson2structがオススメです。

絶賛リファクタリング中なのでCLIのインターフェースは変えないが中身が雑すぎて恥ずかしいので絶対に見ないでほしい。は引き続きお願いします。[Github]

将来直したいこと

  • プロパティ名がアルファベット順になっているので元のJSONの順番になるように合わせたい
  • GopherJSでクリップボードにコピーしたい
  • example tagを完璧に元のJSONに戻せるようにしたい。
  • 出力されるstruct名が被ってしまう場合があるのでその時の対処を考える。(Imageストラクトがいろんなストラクトに含まれている場合など)
<< Kubernetesの成功したJobを消す
Go言語の忘れがちなtime fomatの話 >>
@yudppp
Web engineer.