Go
golang
jsonschema

JSON Schema や JSON から Go の struct を生成する

golang には標準で JSON 用のパッケージがあって、Go で JSON を読み込んだり、書き出したりするときは、そのための struct を定義することがほとんどだと思います。
便利に使わせてもらっているのですが、 struct を定義していくのが結構面倒だったりして、これを自動で生成しようというソフトウェアがいくつかあります。
自分も作ったうちのひとりなのですが、皆さん考えることは同じですね……
機能やできることも異なるので、それぞれの特徴などを紹介していきます。

schematyper

JSON Schema :arrow_right: struct

作者
Igor Dubinskiy
ライセンス
MIT

JSON Schhema の type に null がある要素はポインタで表現されます。(*stringなど)
JSON Schhema で required でない要素は、 omitempty のタグが付きます。
JSON Schhema の description に改行文字を含む場合、 title が英数字でない場合など正常にソースコードを出力できなかったりします。
object の description の内容をコメントで出力します。
format が date-time のものは time.Time とします。(RFC3339であれば動作します)
type: ["string", "integer"] のように異なる型を持つ配列は interface{} として対応しています。

sample

//// $ curl -O "http://qiita.com/api/v2/schema"
//// $ jq '.properties.comment + {"type": "object"}' schema > comment.json
//// # 解析可能な JSON Schema とするため comment.json の .properties.user に {"type": "object"} を追加
//// # ファイル名とtitile から struct 名を決めるようなので `"title": "ユーザ"` を `"title": "user"` に書き換え
//// $ schematyper comment.json
package main

// generated by "schematyper comment.json" -- DO NOT EDIT

import "time"

// 投稿に付けられたコメントを表します。
type comment struct {
    Body         string    `json:"body"`
    CreatedAt    time.Time `json:"created_at"`
    ID           string    `json:"id"`
    RenderedBody string    `json:"rendered_body"`
    UpdatedAt    time.Time `json:"updated_at"`
    User         user      `json:"user"`
}

// Qiita上のユーザを表します。
type user struct {
    Description       *string `json:"description"`
    FacebookID        *string `json:"facebook_id"`
    FolloweesCount    int     `json:"followees_count"`
    FollowersCount    int     `json:"followers_count"`
    GithubLoginName   *string `json:"github_login_name"`
    ID                string  `json:"id"`
    ItemsCount        int     `json:"items_count"`
    LinkedinID        *string `json:"linkedin_id"`
    Location          *string `json:"location"`
    Name              *string `json:"name"`
    Organization      *string `json:"organization"`
    PermanentID       int     `json:"permanent_id"`
    ProfileImageURL   string  `json:"profile_image_url"`
    TwitterScreenName *string `json:"twitter_screen_name"`
    WebsiteURL        *string `json:"website_url"`
}

schematic

JSON Hyper-Schema :arrow_right: HTTP client

Generate Go client code for HTTP APIs described by JSON Hyper-Schemas.

と説明にあるように、 struct の生成というよりは client を生成する。
以下のブログにあるように Fork して改変して利用している例もあるようです。

JSON Schema から API 仕様と Go コードを自動で生成する – BOT エントリーの裏側 Part.1 | eureka tech blog

sample

//// $ curl -O "http://qiita.com/api/v2/schema"
//// $ jq '.properties.comment + {"type": "object"}' schema > comment.json
//// # 解析可能な JSON Schema とするため comment.json の .properties.user に {"type": "object"} を追加
//// # ファイル名とtitile から struct 名を決めるようなので `"title": "ユーザ"` を `"title": "user"` に書き換え、 `"title": "コメント"` を `"title": "comment"` に書き換え
//// $ schematic comment.json
// Generated service client for comment API.
//~~~
// Qiita上のユーザを表します。
type User struct {
    Description     *string `json:"description" url:"description,key"`             // 自己紹介文
    FacebookID      *string `json:"facebook_id" url:"facebook_id,key"`             // Facebook ID
    FolloweesCount  int     `json:"followees_count" url:"followees_count,key"`     // このユーザがフォローしているユーザの数
    FollowersCount  int     `json:"followers_count" url:"followers_count,key"`     // このユーザをフォローしているユーザの数
    GithubLoginName *string `json:"github_login_name" url:"github_login_name,key"` // GitHub ID
    ID              string  `json:"id" url:"id,key"`                               // ユーザID
    ItemsCount      int     `json:"items_count" url:"items_count,key"`             // このユーザが qiita.com 上で公開している投稿の数
    // (Qiita:Teamでの投稿数は含まれません)
    LinkedinID        *string `json:"linkedin_id" url:"linkedin_id,key"`                 // LinkedIn ID
    Location          *string `json:"location" url:"location,key"`                       // 居住地
    Name              *string `json:"name" url:"name,key"`                               // 設定している名前
    Organization      *string `json:"organization" url:"organization,key"`               // 所属している組織
    PermanentID       int     `json:"permanent_id" url:"permanent_id,key"`               // ユーザごとに割り当てられる整数のID
    ProfileImageURL   string  `json:"profile_image_url" url:"profile_image_url,key"`     // 設定しているプロフィール画像のURL
    TwitterScreenName *string `json:"twitter_screen_name" url:"twitter_screen_name,key"` // Twitterのスクリーンネーム
    WebsiteURL        *string `json:"website_url" url:"website_url,key"`                 // 設定しているWebサイトのURL
}

structr

JSON Schema :arrow_right: struct

JSONSchemaからstructのようなコードを生成する"structr"というのを書いた

yaml で書かれた設定ファイルから変換の設定を変更できることが特徴。

sample

//// $ curl -O "http://qiita.com/api/v2/schema"
//// $ structr template > conf.yaml
//// $ jq '.properties.comment | .+ {"type": "object"}' schema > comment.json
//// $ structr generate -c conf.yaml comment.json
////2017/08/16 02:08:22 cannot add load json schema:  json: cannot unmarshal array into Go struct field JsonSchema.type of type main.JsonSchemaType
///// すみません動きませんでした

schemarshal

JSON Schema :arrow_right: struct

作者
aaharu
ライセンス
BSD-2-Clause

筆者作 :writing_hand_tone2:
JSON Schema から "encoding/json" の Mashal に使える構造体を作ることから命名しました。
もっとカッコイイ名前のほうが良かったかなあと思ってたりもします :pensive:

以下に対応することを目的に作りました。
- go generate から扱いやすいように、パイプからの入力、引数での入出力に対応
- JSON Schema のパーサーと、コード生成のロジックを分ける
- enum 対応(MarshalJSON, UnmarshalJSON も生成)

JSON Schhema の type に null がある要素はポインタで表現されます。(*stringなど)
JSON Schhema で required でない要素は、 omitempty のタグが付きます。
format が date-time のものは time.Time とします。(RFC3339であれば動作します)
description の内容をコメントで出力しますが、オプションでコメント出力を切ることもできます。
type: ["string", "integer"] のように異なる型を持つ配列は未対応です。

date-time を time.Time にしたり、 number を float64 とする部分は設定で変更できないので、今後設定で変えられるような仕組みを検討中です。
要望があれば取り込んでいきたいとは思っています。

sample

//// $ curl -s "http://qiita.com/api/v2/schema" | schemarshal
// Code generated by schemarshal 1.2.0 `schemarshal`
// DO NOT RECOMMEND EDITING THIS FILE.

package main
//~~~
// QiitaAPIV2JSONSchemaCommentUserObject : Qiita上のユーザを表します。
type QiitaAPIV2JSONSchemaCommentUserObject struct {
    // Description : 自己紹介文
    Description *string `json:"description"`
    // FacebookID : Facebook ID
    FacebookID *string `json:"facebook_id"`
    // FolloweesCount : このユーザがフォローしているユーザの数
    FolloweesCount int64 `json:"followees_count"`
    // FollowersCount : このユーザをフォローしているユーザの数
    FollowersCount int64 `json:"followers_count"`
    // GithubLoginName : GitHub ID
    GithubLoginName *string `json:"github_login_name"`
    // ID : ユーザID
    ID string `json:"id"`
    // ItemsCount : このユーザが qiita.com 上で公開している投稿の数 (Qiita:Teamでの投稿数は含まれません)
    ItemsCount int64 `json:"items_count"`
    // LinkedinID : LinkedIn ID
    LinkedinID *string `json:"linkedin_id"`
    // Location : 居住地
    Location *string `json:"location"`
    // Name : 設定している名前
    Name *string `json:"name"`
    // Organization : 所属している組織
    Organization *string `json:"organization"`
    // PermanentID : ユーザごとに割り当てられる整数のID
    PermanentID int64 `json:"permanent_id"`
    // ProfileImageURL : 設定しているプロフィール画像のURL
    ProfileImageURL string `json:"profile_image_url"`
    // TwitterScreenName : Twitterのスクリーンネーム
    TwitterScreenName *string `json:"twitter_screen_name"`
    // WebsiteURL : 設定しているWebサイトのURL
    WebsiteURL *string `json:"website_url"`
}

// QiitaAPIV2JSONSchemaCommentObject : 投稿に付けられたコメントを表します。
type QiitaAPIV2JSONSchemaCommentObject struct {
    // Body : コメントの内容を表すMarkdown形式の文字列
    Body string `json:"body"`
    // CreatedAt : データが作成された日時
    CreatedAt time.Time `json:"created_at"`
    // ID : コメントの一意なID
    ID string `json:"id"`
    // RenderedBody : コメントの内容を表すHTML形式の文字列
    RenderedBody string `json:"rendered_body"`
    // UpdatedAt : データが最後に更新された日時
    UpdatedAt time.Time `json:"updated_at"`
    // User : Qiita上のユーザを表します。
    User QiitaAPIV2JSONSchemaCommentUserObject `json:"user"`
}
//~~~

gojson

JSON :arrow_right: struct

作者
ChimeraCoder
ライセンス
GPL-3.0

紹介した他のものとは異なり、JSON Schemaからではなく、既存のJSONから生成するのが特徴。
そのため、既に公開済み、実装済みのAPIレスポンスから定義を生成するのに向いていると思います。

gojson コマンドは、パイプからの入力、引数での入出力に対応し、 go generate にも組み込みやすい作りです :thumbsup_tone2:
nest した struct というのでしょうか、、結果は1つの大きな struct を生成します。
null を含むデータから生成しましたが、 string として判定されました。

sample

//// $ curl -s "http://qiita.com/api/v2/items/c686397e4a0f4f11683d/comments" | gojson
package main

type Foo []struct {
    Body         string `json:"body"`
    CreatedAt    string `json:"created_at"`
    ID           string `json:"id"`
    RenderedBody string `json:"rendered_body"`
    UpdatedAt    string `json:"updated_at"`
    User         struct {
        Description       string `json:"description"`
        FacebookID        string `json:"facebook_id"`
        FolloweesCount    int64  `json:"followees_count"`
        FollowersCount    int64  `json:"followers_count"`
        GithubLoginName   string `json:"github_login_name"`
        ID                string `json:"id"`
        ItemsCount        int64  `json:"items_count"`
        LinkedinID        string `json:"linkedin_id"`
        Location          string `json:"location"`
        Name              string `json:"name"`
        Organization      string `json:"organization"`
        PermanentID       int64  `json:"permanent_id"`
        ProfileImageURL   string `json:"profile_image_url"`
        TwitterScreenName string `json:"twitter_screen_name"`
        WebsiteURL        string `json:"website_url"`
    } `json:"user"`
}

JSON-to-Go

JSON :arrow_right: struct

go generate での使用を想定した他のものとは異なり、 JavaScript で書かれていてブラウザで動作します。
やっていることは gojson のブラウザ版という感じでしょうか。

null を含むデータは、 interface{} として判定されました。

sample

//// $ curl -s "http://qiita.com/api/v2/items/c686397e4a0f4f11683d/comments"
//// の結果を JSON-to-Go に貼り付けました。
type AutoGenerated []struct {
    Body         string    `json:"body"`
    CreatedAt    time.Time `json:"created_at"`
    ID           string    `json:"id"`
    RenderedBody string    `json:"rendered_body"`
    UpdatedAt    time.Time `json:"updated_at"`
    User         struct {
        Description       interface{} `json:"description"`
        FacebookID        interface{} `json:"facebook_id"`
        FolloweesCount    int         `json:"followees_count"`
        FollowersCount    int         `json:"followers_count"`
        GithubLoginName   string      `json:"github_login_name"`
        ID                string      `json:"id"`
        ItemsCount        int         `json:"items_count"`
        LinkedinID        interface{} `json:"linkedin_id"`
        Location          interface{} `json:"location"`
        Name              string      `json:"name"`
        Organization      interface{} `json:"organization"`
        PermanentID       int         `json:"permanent_id"`
        ProfileImageURL   string      `json:"profile_image_url"`
        TwitterScreenName string      `json:"twitter_screen_name"`
        WebsiteURL        interface{} `json:"website_url"`
    } `json:"user"`
}

まとめ

struct を生成する、ということに注目すると、 schematyper, schemarshal, gojson, JSON-to-Go あたりが向いています。
schematyper と schemarshal はほぼ同等の機能を持っていて、 enum や異なる型を持つ配列あたりの対応に差があります。

既にある API のレスポンスをパースしたいというだけなら JSON-to-Go を使うのが一番楽だと思います。
ただしその API がクエリなどによってレスポンスに含まれる要素が異なったりする場合、 結果に手を入れたりすることなどが必要になってきます。( omitempty の判断などができない)
go generate で実行したい場合などは gojson を使うのも良いかもしれません。

API 提供側で JSON Schema が用意してあったり、 API と JSON Schema を新規作成する場合などは、 schematyper か schemarshal を使用すると楽ができるんじゃないかと思います。