Friday the 13th - Gopher vs JSON!!

  • 34
    Like
  • 0
    Comment

今日は13日の金曜日!
ということでせっかくなのでJSONネタで
gopherVSjson.gif
The Gopher character is based on the Go mascot designed by Renée French.

encoding/json

GoにはJSONを扱う標準パッケージが含まれています。
encoding/json
3rd partyのJSONライブラリでも探してきて試そうかとも思ったのですがそれ以前に標準パッケージを全然把握していない自分に気付いたので、そちらを試すことにしました。

エンコード

手っ取り早くエンコードするにはMarshal関数を使います。インデント整形したい場合はMarshalIndentを使います。

structのエンコード

structをJSONオブジェクトにエンコードします。

encode1.json
package main

import (
    "encoding/json"
    "log"
)

type Book struct {
    Title   string
    Price   int
    Authors []string
}

type Bookshelf struct {
    Books   []Book
}

func main(){
    bs := Bookshelf{
        []Book{
            {"Gopher vs JSON!", 1000, []string{"山田 太郎", "鈴木 一郎"}},
            {"Go phrasebook", 2000, []string{"ほげ ぼすけて"}},
            {"Go! Gopher", 3000, []string{"テスト"}},
        },
    }
    b, err := json.MarshalIndent(bs, "", "\t")
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%s", b)
}

結果

{
    "Books": [
        {
            "Title": "Gopher vs JSON!",
            "Price": 1000,
            "Authors": [
                "山田 太郎",
                "鈴木 一郎"
            ]
        },
        {
            "Title": "Go phrasebook",
            "Price": 2000,
            "Authors": [
                "ほげ ぼすけて"
            ]
        },
        {
            "Title": "Go! Gopher",
            "Price": 3000,
            "Authors": [
                "テスト"
            ]
        }
    ]
}

Marshal関数の戻り値はUTF8でエンコードされたbyte配列です。

structの場合は公開されたフィールドのみエンコードされます。フィールド名がそのままJSONオブジェクトのキーになるので、このままだと大文字開始のキーしか出力できません。またnil値がJSONのnullにエンコードされるのですが、string等nilを代入出来ない型だとnullを出力できません。
タグ(Javaでいうところのannotationみたいなもの?)をつかうことでエンコードルールをもう少し細かく制御することが出来ます。

type Book struct {
    Title   string   `json:"title"`
    Price   int      `json:"price,omitempty"`
    Authors []string `json:"authors"`
    Secret  string   `json:"-"`
}

json:"title"はJSONのキーを"title"とすることを表します。
omitemptyオプションが指定されたフィールドは0値のときにエンコード非対象となります。JSONで0値とnilを区別したいときはどうすればいいのかな。ちょっと気持ち悪いけどinterface{}型にするしかないか。
公開したいけどJSONには出力したくないフィールドにはjson:"-"と記述します。

mapのエンコード

mapもstructと同様にエンコードできます。下記サンプルコードはstructのエンコードサンプルをmapに書き直したものです。mapのキーの型はstringである必要があります。値の型は何が入るか分からないのでinterface{}型にしてます。

encode2.go
package main

import (
    "encoding/json"
    "log"
)

func main(){
    bs := map[string]interface{}{
        "Books": []map[string]interface{} {
            map[string]interface{} {
                "Title" : "Gopher vs JSON!",
                "Price" : 1000,
                "Authors" : []string{"山田 太郎", "鈴木 一郎"},
            },
            map[string]interface{} {
                "Title" : "Go phrasebook",
                "Price" : 2000,
                "Authors" : []string{"山田 太郎", "鈴木 一郎"},
            },
            map[string]interface{} {
                "Title" : "Go! Gopher",
                "Price" : 3000,
                "Authors" : []string{"テスト"},
            },
        },
    }
    b, err := json.MarshalIndent(bs, "", "\t")
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("%s", b)
}

Marshaler

Marshalerインターフェースを実装することでtypeに対して独自のエンコード処理を実装することが出来ます。下記コードではAuthorsというtypeを定義してMarshalJSONメソッドを実装し、stringスライスをハイフン区切りのJSON文字列として出力する様にしました。※エスケープ処理は割愛しています

encode4.go

type Authors []string

func (a Authors) MarshalJSON() ([]byte, error) {
    return []byte("\"" + strings.Join(a, "-") + "\""), nil
}

type Book struct {
    Title   string  `json:"title"`
    Price   int     `json:"price,omitempty"`
    Authors Authors `json:"authors"`
    Secret  string  `json:"-"`
}

type Bookshelf struct {
    Books []Book `json:"books"`
}

出力

{
    "books": [
        {
            "title": "Gopher vs JSON!",
            "price": 1000,
            "authors": "山田 太郎-鈴木 一郎"
        },
        {
            "title": "Go phrasebook",
            "price": 2000,
            "authors": "ほげ ぼすけて"
        },
        {
            "title": "Go! Gopher",
            "price": 3000,
            "authors": "テスト"
        },
        {
            "title": "",
            "authors": "hoge"
        }
    ]
}

どうでもいいことですがMarshalerのスペル、L2個じゃないのかな?

Encoder

Encoderを使用するとストリーム(io.Writer)に対してJSONエンコードを連続出力することができます。

encode5.go
package main

import (
    "encoding/json"
    "log"
    "os"
)

type Book struct {
    Title   string   `json:"title"`
    Price   int      `json:"price,omitempty"`
    Authors []string `json:"authors"`
}

func main() {
    enc := json.NewEncoder(os.Stdout)

    if err := enc.Encode(&Book{"Gopher vs JSON!", 1000, []string{"山田 太郎", "鈴木 一郎"}}); err != nil {
        log.Fatal(err)
    }
    if err := enc.Encode(&Book{"Go phrasebook", 2000, []string{"ほげ ぼすけて"}}); err != nil {
        log.Fatal(err)
    }
    if err := enc.Encode(&Book{"Test", 2000, []string{"ZZZZ"}}); err != nil {
        log.Fatal(err)
    }
}

出力

{"title":"Gopher vs JSON!","price":1000,"authors":["山田 太郎","鈴木 一郎"]}
{"title":"Go phrasebook","price":2000,"authors":["ほげ ぼすけて"]}
{"title":"Test","price":2000,"authors":["ZZZZ"]}

デコード

structへのデコード

デコードにはUnmarshal関数を使用します。
定義済みstructのポインタを引数に渡せば型をよしなに変換して格納してくれます。

decode3.go
package main

import (
    "encoding/json"
    "log"
)

type Book struct {
    Title   string   `json:"title"`
    Price   int      `json:"price,omitempty"`
    Authors []string `json:"authors"`
    Secret  string   `json:"-"`
}

type Bookshelf struct {
    Books []Book `json:"books"`
}

func main() {

    testJson := `
{
    "books": [
        {
            "title": "Gopher vs JSON!",
            "price": 1000,
            "authors": [
                "山田 太郎",
                "鈴木 一郎"
            ]
        },
        {
            "title": "Go phrasebook",
            "price": 2000,
            "authors": [
                "ほげ ぼすけて"
            ]
        },
        {
            "title": "Go! Gopher",
            "price": 3000,
            "authors": [
                "テスト"
            ]
        },
        {
            "title": "",
            "authors": [
                "hoge"
            ]
        }
    ]
}
`

    var b Bookshelf
    if err := json.Unmarshal([]byte(testJson), &b); err != nil {
        log.Fatal(err)
    }

    log.Printf("%s", b)
}

結果

{[{Gopher vs JSON! %!s(int=1000) [山田 太郎 鈴木 一郎] } {Go phrasebook %!s(int=2000) [ほげ ぼすけて] } {Go! Gopher %!s(int=3000) [テスト] } { %!s(int=0) [hoge] }]}

恣意的なデータのデコード

事前に構造が明確なでないJSONをデコードする場合、またはstructを用意するのが面倒な場合はinterfece{}変数を渡してデコードできます。

decode5.go
    var b interface{}
    if err := json.Unmarshal([]byte(testJson), &b); err != nil {
        log.Fatal(err)
    }

結果

map[books:[map[title:Gopher vs JSON! price:%!s(float64=1000) authors:山田 太郎,鈴木 一郎] map[title:Go phrasebook price:%!s(float64=2000) authors:ほげ ぼすけて] map[title:Go! Gopher price:%!s(float64=3000) authors:テスト] map[title: authors:hoge]]]

この場合キーがstringで値がinterface{}のmapが構築されているので型アサーションつかって値を取り出します。

    bookshelf := b.(map[string]interface{})
    books := bookshelf["books"].([]interface{})
    book2 := books[2].(map[string]interface{})
    log.Printf("%s\n", book2["title"])

型アサーションちょっと面倒ですが、JSON全体を再帰的にパースする場合などはtype switchと併用すれば便利そうです。

Unmarshaler

Unmarshalerインターフェースを実装することで独自デコード処理を記述することが出来ます。
以下かなり手抜きですがMarsharlerのサンプルで出力したハイフン区切りデータを復号しています。

decode4.go
package main

import (
    "encoding/json"
    "log"
    "strings"
)

type Authors []string

func (a *Authors) UnmarshalJSON(b []byte) error {
    s := strings.TrimPrefix(string(b), "\"")
    s = strings.TrimSuffix(s, "\"")
    *a = strings.Split(s, "-")
    return nil
}

type Book struct {
    Title   string  `json:"title"`
    Price   int     `json:"price,omitempty"`
    Authors Authors `json:"authors"`
    Secret  string  `json:"-"`
}

type Bookshelf struct {
    Books []Book `json:"books"`
}

func main() {

    testJson := `
{
    "books": [
        {
            "title": "Gopher vs JSON!",
            "price": 1000,
            "authors": "山田 太郎-鈴木 一郎"
        },
        {
            "title": "Go phrasebook",
            "price": 2000,
            "authors": "ほげ ぼすけて"
        },
        {
            "title": "Go! Gopher",
            "price": 3000,
            "authors": "テスト"
        }
    ]
}
`
    var b Bookshelf
    if err := json.Unmarshal([]byte(testJson), &b); err != nil {
        log.Fatal(err)
    }

    log.Printf("%s", b)
}

Decoder

Decoderを使用するとストリーム(io.Reader)から連続してJSONを読み込みます。

decode6.go
package main

import (
    "encoding/json"
    "log"
    "strings"
)

type Book struct {
    Title   string   `json:"title"`
    Price   int      `json:"price,omitempty"`
    Authors []string `json:"authors"`
}

func main() {
    testJson := `
    {
        "title": "Gopher vs JSON!",
        "price": 1000,
        "authors": [
        "山田 太郎",
            "鈴木 一郎"
        ]
    }
    {
        "title": "Go phrasebook",
        "price": 2000,
        "authors": [
            "ほげ ぼすけて"
        ]
    }
    {
        "title": "Go! Gopher",
        "price": 3000,
        "authors": [
            "テスト"
        ]
    }
`

    dec := json.NewDecoder(strings.NewReader(testJson))

    for {
        var b Book
        if err := dec.Decode(&b); err != nil {
            log.Fatal(err)
        }
        log.Printf("%s", b)
    }
}

出力

2013/12/13 08:29:52 {Gopher vs JSON! %!s(int=1000) [山田 太郎 鈴木 一郎]}
2013/12/13 08:29:52 {Go phrasebook %!s(int=2000) [ほげ ぼすけて]}
2013/12/13 08:29:52 {Go! Gopher %!s(int=3000) [テスト]}
2013/12/13 08:29:52 EOF
exit status 1

最後はDecodeがEOFを返すんですね。

まとめ

以上です。
今回本当はMartini試す記事を書きたかったのだけど、その前にJSONを少しおさらいしとこうと思っていたら時間がなくなってしまいました。orz
普段業務ではJavaでJSONIC使ってるのですがencoding/json使い勝手も似ていて取っ付きやすいです。タグの使いどころもイイ感じ。ちょっとモヤモヤする部分もありますが・・・

今回の記事で書いたコード

https://github.com/hogedigo/gophervsjson/tree/master/src/cmd

参考資料

encoding/json
JSON and Go