JSON の操作

某所であった JSON データの操作に関するやり取りが面白かったのでネタにしてみる。

まず,サンプルの JSON データとして以下のデータを使う。

var sampleJSON = `{
  "books": [
    {
      "Type": "aozora",
      "ID": "56839",
      "Title": "ニャルラトホテプ",
      "OriginalTitle": "NYARLATHOTEP",
      "URL": "https://www.aozora.gr.jp/cards/001699/card56839.html",
      "ProductType": "青空文庫",
      "Creators": [
        {
          "Name": "ラヴクラフト ハワード・フィリップス"
        },
        {
          "Name": "大久保 ゆう",
          "Role": "翻訳"
        }
      ],
      "PublicationDate": "2014-04-04",
      "LastRelease": "2015-08-19"
    },
    {
      "Type": "aozora",
      "ID": "4307",
      "Title": "グリゴリの捕縛",
      "URL": "https://www.aozora.gr.jp/cards/000021/card4307.html",
      "ProductType": "青空文庫",
      "Creators": [
        {
          "Name": "白田 秀彰"
        }
      ],
      "PublicationDate": "2001-11-26",
      "LastRelease": "2014-09-17"
    }
  ]
}`

このデータを簡単に扱う方法を考える。

とりあえず map にデコードする

お手軽なのは JSON オブジェクトを map[string]interface に,配列を []interface にデコードすることである。 たとえばこんな感じ。

func main() {
    var decodeData map[string]interface{}
    if err := json.Unmarshal([]byte(sampleJSON), &decodeData); err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    book0 := decodeData["books"].([]interface{})[0]
    creators := book0.(map[string]interface{})["Creators"].([]interface{})
    fmt.Println(creators)
    // Output:
    // [map[Name:ラヴクラフト ハワード・フィリップス] map[Name:大久保 ゆう Role:翻訳]]
}

ただ,このやり方では map や interface にデコードしたデータを取り出す際にキャストの嵐になるので,どうしても記述が煩雑になる。

koron/go-dproxy パッケージを使う

上述のデコードしたデータをもう少し簡単に扱うために koron/go-dproxy パッケージを利用する。 こんな感じ。

func main() {
    var decodeData map[string]interface{}
    if err := json.Unmarshal([]byte(sampleJSON), &decodeData); err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    a, err := dproxy.New(decodeData).M("books").A(0).M("Creators").Array()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    fmt.Println(a)
    // Output:
    // [map[Name:ラヴクラフト ハワード・フィリップス] map[Name:大久保 ゆう Role:翻訳]]
}

エラーハンドリングも含めてかなりスッキリ書ける。 更に koron/go-dproxy パッケージは JSON Pointer (RFC 6901) にも対応しているそうで

func main() {
    var decodeData map[string]interface{}
    if err := json.Unmarshal([]byte(sampleJSON), &decodeData); err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }

    a, err := dproxy.New(decodeData).P("/books/0/Creators").Array()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    fmt.Println(a)
    // Output:
    // [map[Name:ラヴクラフト ハワード・フィリップス] map[Name:大久保 ゆう Role:翻訳]]
}

といった書き方もできる。 便利!

JSON デコード用構造体を生成するツールを利用する

利用する JSON データの構成があらかじめ分かっているのなら yudppp/json2struct を使って JSON データから構造体を生成することができる。

yudppp/json2struct はコマンドラインツールとして利用する。 こんな感じ。

$ cat sample.json | json2struct -name=Booksdata
type Booksdata struct {
    Books []BooksdataBook `json:"books"`
}

type BooksdataBook struct {
    Creators        []BooksdataBookCreator `json:"Creators"`
    ID              string                 `json:"ID"`
    LastRelease     string                 `json:"LastRelease"`
    OriginalTitle   string                 `json:"OriginalTitle"`
    ProductType     string                 `json:"ProductType"`
    PublicationDate string                 `json:"PublicationDate"`
    Title           string                 `json:"Title"`
    Type            string                 `json:"Type"`
    URL             string                 `json:"URL"`
}

type BooksdataBookCreator struct {
    Name string `json:"Name"`
    Role string `json:"Role"`
}

このコードを組み込んで使えばよい。 こんな感じ。

func main() {
	var decodeData Booksdata
	if err := json.Unmarshal([]byte(sampleJSON), &decodeData); err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}

	fmt.Println(decodeData.Books[0].Creators)
	// Output:
	// [{ラヴクラフト ハワード・フィリップス } {大久保 ゆう 翻訳}]
}

jq ぽい操作が欲しい

もっとラフに jq ぽく JSON データを扱えないか。 たとえば savaki/jq を使えばできる。

func main() {
	op, _ := jq.Parse(".books.[0].Creators")
	v, _ := op.Apply([]byte(sampleJSON))

	fmt.Println(string(v))
	// Output:
    // [{"Name":"ラヴクラフト ハワード・フィリップス"},{"Name":"大久保 ゆう","Role":"翻訳"}]
}

savaki/jq は JSON データのフィルタとして機能する。 操作としては savaki/jq で必要な部分を抽出した後,これまで述べた方法で操作する,といった感じに組み合わせて使うといいかもしれない。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-05-21)
Kindle版
B094PRR7PZ (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)