Go言語

【Go言語】encoding/jsonパッケージの使い方。Goブログ「JSON and Go」を和訳してみた

Webアプリケーションを実装する上で必ず使うであろう json パッケージについて、復習のためGoブログ JSON and Go を読みました。

せっかく読んだので和訳を記事にしようと。

Encoding

JSONデータをエンコードするには、Marshal 関数を使う。

func Marshal(v interface{}) ([]byte, error)

 

次のような Message 構造体とそのインスタンス m があるとする。

type Message struct {
    Name string
    Body string
    Time int64
}

m := Message{"Alice", "Hello", 1294706395881547000}

 

json.Marshal を使えばJSONエンコードされた m を得ることができます。

b, err := json.Marshal(m)

 

エラーなくうまくいけば errnilb はJSONデータを含む []byte 型になります。

全体のコードは↓な感じ。

package main

import (
	"fmt"
	"encoding/json"
)

type Message struct {
    Name string
    Body string
    Time int64
}

func main() {
	m := Message{"Alice", "Hello", 1294706395881547000}
	b, err := json.Marshal(m)
	if err == nil {
		fmt.Printf("%s\n", b) // {"Name":"Alice","Body":"Hello","Time":1294706395881547000}
	}
}

 

有効なJSONとして表現できるデータ構造のみがエンコードされます。

  • JSONオブジェクトはキーには文字列のみサポート。Go の map をエンコードするには、map[string]T である必要がある。(T は json パッケージがサポートしているGoの任意の型)
  • Channel, complex, function 型はエンコードできない。
  • 循環データ構造はサポートしていない。無限ループを引き起こす。
  • ポインタはそれらが指す値にエンコードされる。ポインタが nil なら null になる。

json パッケージはパブリックのフィールドのみアクセスできる。(public, 大文字で始まるフィールド)
そのためパブリックのフィールドのみがJSONエンコードされる(JSONデータに含まれる)

 

Decoding

JSONデータをデコードするときは  Unmarshal 関数を使う。

func Unmarshal(data []byte, v interface{}) error

 

まず最初にすることは、デコードしたデータを保存する変数を用意すること。

var m Message

 

そして、引数に JSONデータ( []byte )と  m のポインタ(デコード結果を保存する変数のポインタ)をわたして json.Unmarshal を呼び出す。

err := json.Unmarshal(b, &m)

 

bm に適合する有効なJSONであれば、errnil で次のように構造体 mb をデコードしたデータが格納される。

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

 

Unmarshal のサンプルコードは↓な感じ。

package main

import (
	"fmt"
	"encoding/json"
)

type Message struct {
    Name string
    Body string
    Time int64
}

func main() {
	b := []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)
	var m Message
	if err := json.Unmarshal(b, &m); err != nil {
		fmt.Println("Failed Unmarshal err: ", err)
		return
	}
	fmt.Printf("m = %+v\n", m) // m = {Name:Alice Body:Hello Time:1294706395881547000}
}

 

Unmarshal は構造体のフィールド定義にある json タグを探索してデコードしたデータを格納するフィールドを特定している。

探索の優先順位は

JSONのキーが "Foo" の場合

  1. json タグに "Foo" が指定されているパブリックのフィールド
  2. パブリックのフィールドでフィールド名が "Foo" のもの
  3. パブリックのフィールドでフィールド名が "FoO" や "FOO" のように大文字・小文字を区別せず "Foo" に一致するもの

JSONデータの構造がGoの型と完全に一致しない場合はどうなるのか?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

 

Unmarshal は一致するフィールのみをデコードする。このケースでは、Name フィールドのみデコードされ Food フィールドは無視される。この挙動は巨大なJSONデータから少しの特定のキーのみピックアップして取得したいときに役立つ。また、プライベートなフィールドは Unmarshal の影響を受けないことも意味している。

しかし、JSONデータの構造が事前にわからない場合はどうするのか?

 

Generic JSON with interface{}

interface {}(空のインターフェース)型は、メソッドがゼロのインターフェイス。そのため、Goのすべての型は空のインターフェースを満たす。

空のインターフェースは、どんな値でも入るコンテナ型(入れ物)として機能する。

var i interface{}
i = "a string"
i = 2011
i = 2.777

型アサーションは、基になる具象型にアクセスする。

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

 

基になる型が不明な場合は switch を使う。

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i isn't one of the types above
}

 

json パッケージは任意のJSONオブジェクトや配列を格納するのに  map[string]interface{} と []interface{} を使う。有効なJSONをプレーンな interface{} の値に unmarshal する。

  • bool は JSON の boolean に
  • float64 は JSON の numbers に
  • string は JSON の strings に
  • nil は JSON の null に

 

Decoding arbitrary data

変数b に格納されたJSONデータがあるとする。

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

 

データ構造がわからない場合、Unmarshal でデコードした結果を interface{} に格納する。

var f interface{}
err := json.Unmarshal(b, &f)

 

この時点では、f はキーが文字列でバリューが空のインターフェースとして格納された値のmapになっている。

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

 

データにアクセスするために fmap[string]interface{} に型アサーションする。

m := f.(map[string]interface{})

 

そして、m を反復処理して型変換してバリューにアクセスする。

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

 

このようにして、型安全の恩恵を受けながら、未知の構造のJSONを処理する。

コードの全体像は↓な感じ。

package main

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

func main() {
	b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
	var f interface{}
	json.Unmarshal(b, &f)
	if err := json.Unmarshal(b, &f); err != nil {
		log.Fatalln("failed unmarshal. err: ", err)
	}
	fmt.Printf("f = %#v\n", f) // f = map[string]interface {}{"Age":6, "Name":"Wednesday", "Parents":[]interface {}{"Gomez", "Morticia"}}
	m := f.(map[string]interface{})
	fmt.Printf("m = %#v\n", m) // m = map[string]interface {}{"Age":6, "Name":"Wednesday", "Parents":[]interface {}{"Gomez", "Morticia"}}
	for k, v := range m {
		switch vv := v.(type) {
		case string:
			fmt.Printf("%s is string. %s\n", k, vv) // Name is string. Wednesday
		case float64:
			fmt.Printf("%s is float64. %f\n", k, vv) // Age is float64. 6.000000
		case interface{}:
			fmt.Printf("%s is interface{}. %#v\n", k, vv) // Parents is interface{}. []interface {}{"Gomez", "Morticia"}
		}
	}
}

 

Reference Types

先程の例のJSONデータを含むGoの型を定義されているとします。

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

 

期待通りデータを FamilyMember にアンマーシャリングしますが、よく見ると驚くべきことが起こっている。

var ステートメントで FamilyMember 構造体を割り当て、Unmarshal 関数に変数のポインタを渡したとき、Parents フィールドは nilスライスである。Parents フィールドに値を入れるため、裏で Unmarshal 関数が新しいスライスを割り当てている。これは Unmarshal が参照型(ポインタ、スライス、マップ)でどう機能するかを示す典型。

次の構造体にアンマーシャリングする例を考えてみる。

type Foo struct {
    Bar *Bar
}

 

JSONオブジェクトに Bar キーがあれば、Unmarshal 関数は新たに Bar を割り当てる。もしなかったら、Bar はnilポインタのまま。

この特徴を活かした有用なパターンがある。異なるタイプのメッセージを受信するアプリケーションがあるときは、次のような「receiver」構造体を定義すると方法がある。

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

 

そして、送信側は通信するメッセージのタイプに応じて、トップレベルのJSONオブジェクトの Cmdフィールドや Msgフィールドにデータを入力する。 Unmarshal は、JSONを IncomingMessage 構造体にデコードするときに、JSONデータに存在するデータ構造のみを割り当てるため、処理するメッセージを知るには Cmd または Msg のいずれかが nil でないことをテストするだけで良い。

 

Streaming Encoders and Decoders

jsonパッケージにはJSONデータのストリームの読み書き操作をサポートしている Decoder と Encoder 型がある。NewDecoder とNewEncoder 関数は io.Reader と io.Writer インターフェースをラップしている。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

 

これは、標準入力からの一連のJSONオブジェクトを読み込み、各オブジェクトの Name フィールドを削除して、標準出力にオブジェクトを書き込むというサンプルプログラム。

package main

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

func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

 

io.Readerio.Writer は広く使われているため、 Decoder と Encoder 型は、HTTP接続、WebSocket、ファイルの読み取りと書き込みなど、幅広いシナリオで使用できる。

 

【追加】 APIのレスポンスをデコードするサンプルコード

net/http パッケージを使って、GETリクエストを投げ、レスポンスをJSONデコードする例。

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type Data struct {
	Hoge string
	Fuga int
}

func getData() (Data, error) {
	url := "http://example.com/api"
	res, err := http.Get(url)
	var data Data
	if err != nil {
		return data, err
	}
	defer res.Body.Close()

	if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
		return data, err
	}

	return data, nil
}

 

参考

Goブログ JSON and Go

 

おすすめ図書

Tour of Goを終えた人にオススメです!!

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語