7.2 JSONの処理

JSON(Javascript Object Notation)は軽量なデータ記述言語です。文字を基礎とした言語のテキスト形式で、C言語ファミリーに似た習慣を採用しています。JSONとXMLの最も大きな違いはXMLが完全なマークアップ言語であるのに対し、JSONがそうでない点です。JSONはXMLに比べ小さく、早く簡単に解析でき、ブラウザのビルトインの素早い解析のサポートもあり、ネットワークのデータ転送分野により適しています。現在我々が見ることのできる多くのオープンプラットフォームでは基本的にJSONをデータ交換のインターフェースとして採用しています。JSONはWeb開発の中でもこのように重要でありますから、Go言語ではJSONのサポートはどうなっているのでしょうか?Go言語の標準ライブラリはすでに非常に良くJSONをサポートしています。JSONデータに対してとても簡単にエンコード/デコードといった作業を行うことができます。

前の節の操作の例でJSONを使って表示しました。結果は以下の通りです:

{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}

この節の残りの内容はこのJSONデータをもとに、go言語のjsonパッケージによるJSONデータのエンコード/デコードをご紹介します。

JSONの解析

構造体に解析する

上のようなJSON文字列があると仮定します。ではどのようにこのJSON文字列を解析するのでしょうか?GoのJSONパッケージには以下のような関数があります

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

この関数を使って解析の目的を実現することができます。詳細な解析の例は以下のコードをご覧ください:

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    ServerName string
    ServerIP   string
}

type Serverslice struct {
    Servers []Server
}

func main() {
    var s Serverslice
    str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
    json.Unmarshal([]byte(str), &s)
    fmt.Println(s)
}

上のコード例の中では、まずjsonデータに対応する構造体を定義します。配列はsliceに、フィールド名はJSONの中のKEYに相当します。解析の際どのようにjsonデータとstructフィールドをマッチさせるのでしょうか?例えばJSONのkeyがFooであった場合、どのようにして対応するフィールドを探すのでしょうか?

  • まずtagに含まれるFooのエクスポート可能なstructフィールド(頭文字が大文字)を探し出します。
  • 次にフィールド名がFooのエクスポートフィールドを探し出します。
  • 最後にFOOまたはFoOのような頭文字を除いたその他の大文字小文字を区別しないエクスポートフィールドを探し出します。

聡明なあなたであればお気づきかもしれません:代入されうるフィールドはエクスポート可能なフィールドである必要があります。(すなわち、頭文字が大文字であるということです。)同時にJSONを解析する際探しだせるフィールドを解析するのみで、探せないフィールドについては無視されます。これのメリットは:とても大きなJSONデータ構造を受け取ってしまいその中のいち部分のデータだけを取得したいような場合です。あなたは必要なデータに対応するフィールド名の大文字だけで簡単にこの問題を解決することができます。

interfaceに解析する

上のような解析方法は解析されるJSONデータの構造を事前に知っている場合に採用されるソリューションです。もし解析されるデータの形式が事前に分からなかった場合はどのように解析すればよいでしょうか?

我々はinterface{}に任意のデータ型のオブジェクトを保存できることを知っています。このようなデータ構造は未知の構造のjsonデータの解析結果を保存するのにぴったりです。JSONパッケージではmap[string]interface{}と[]interface{}構造を採用して任意のJSONオブジェクトと配列を保存します。Goの型とJSONの型の対応関係は以下の通りです:

  • bool は JSON booleans を表します,
  • float64 は JSON numbers を表します,
  • string は JSON string を表します,
  • nil は JSON null を表します,

現在以下のようなJSONデータがあるものと仮定します

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

この構造を知らない段階では、これをinterface{}の中に解析します。

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

この時fの中にはmap型が保存されます。これらのkeyはstringで、値は空のinterface{]の中に保存されます。

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

ではどのようにしてこれらのデータにアクセスするのでしょうか?アサーションによる方法です:

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

アサーションを通して以下のような方法で中のデータにアクセスすることができます。

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case int:
        fmt.Println(k, "is int", 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")
    }
}

上の例では、interface{}とtype assertの組み合わせによって未知の構造のJSONデータを解析することができました。

これはオフィシャルが提供するソリューションです。実は多くの場合、型のアサーションは操作からしてあまり便利ではありません。現在bitly社ではsimplejsonと呼ばれるパッケージがオープンに開発されています。未知の構造体のJSONを処理する場合にとても便利です。詳細な例は以下の通り:

js, err := NewJson([]byte(`{
    "test": {
        "array": [1, "2", 3],
        "int": 10,
        "float": 5.150,
        "bignum": 9223372036854775807,
        "string": "simplejson",
        "bool": true
    }
}`))

arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()

このようにこのライブラリを使用してJSONを操作するのはオフィシャルパッケージに比べてとても簡単です。詳細は以下のアドレスをご参照ください:https://github.com/bitly/go-simplejson

JSONを生成する

多くのアプリケーションを開発する際、最後はJSONデータ文字列を出力する必要があります。ではどのようにして処理するのでしょうか?JSONパッケージではMarshal関数を通して処理します。関数の定義は以下の通り:

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

上のサーバのリスト情報を出力する必要があるものと仮定します。どのように処理すべきでしょうか?下の例をご覧ください:

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    ServerName string
    ServerIP   string
}

type Serverslice struct {
    Servers []Server
}

func main() {
    var s Serverslice
    s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
    s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
    b, err := json.Marshal(s)
    if err != nil {
        fmt.Println("json err:", err)
    }
    fmt.Println(string(b))
}

下のような内容が出力されます:

{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}

上で出力されたフィールド名の頭文字はどれも大文字です。もし頭文字に小文字を使いたい場合はどうすればよいでしょうか?構造体のフィールド名の頭文字を小文字にすべきでしょうか?JSONで出力する際に注意が必要なのは、エクスポートされたフィールドのみが出力されるということです。もしフィールド名を修正してしまうと、何も出力されなくなってしまいます。ですので必ずstruct tagによって定義した上で実装する必要があります:

type Server struct {
    ServerName string `json:"serverName"`
    ServerIP   string `json:"serverIP"`
}

type Serverslice struct {
    Servers []Server `json:"servers"`
}

上の構造体の定義を修正することで、出力されるJSON文字列と我々が最初に定義したJSON文字列は一致します。

JSONの出力に対して、struct tagを定義する場合注意すべきいくつかのことは:

  • フィールドのtagが"-"であった場合、このフィールドはJSONに出力されません。
  • tagにカスタム定義の名前が含まれる場合、このカスタム定義された名前はJSONのフィールド名に出現します。例えば上の例のserverNameに当たります。
  • tagに"omitempty"オプションが含まれる場合、このフィールドの値が空であればJSON文字列の中には出力されません。
  • もしフィールドの型がbool, string, int, int65等で、tagに",string"オプションが含まれる場合、このフィールドがJSONに出力される際はこのフィールドに対応した値が変換されてJSON文字列となります。

例をあげてご説明しましょう:

type Server struct {
    // ID はJSONの中にエクスポートされません。
    ID int `json:"-"`

    // ServerName2 の値は二次JSONエンコーディングが行われます。
    ServerName  string `json:"serverName"`
    ServerName2 string `json:"serverName2,string"`

    // もし ServerIP が空であれば、JSON文字列の中には出力されません。
    ServerIP   string `json:"serverIP,omitempty"`
}

s := Server {
    ID:         3,
    ServerName:  `Go "1.0" `,
    ServerName2: `Go "1.0" `,
    ServerIP:   ``,
}
b, _ := json.Marshal(s)
os.Stdout.Write(b)

以下のような内容が出力されます:

{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}

Marshal関数は変換に成功した時のみデータを返します。変換の過程で注意しなければならないのは:

  • JSONオブジェクトはstringのみをkeyとしてサポートします。そのためmapをエンコードしたい場合は必ずmap[string]Tのような型となります。(TはGo言語の中の任意の型です。)
  • Channel, complexとfunctionはJSONにはエンコードされません。
  • ネストしたデータはエンコードされません。さもないとJSONのエンコードは永久ループに入ってしまいます。
  • ポインタがエンコードされる際はポインタが指定している内容が出力されます。空のポインタはnullを出力します。

この節ではどのようにGo言語のjson標準パッケージを使ってJSONデータをエンコードするかご紹介しました。同時にどのようにサードパーティパッケージであるgo-simplejsonを使っていくつかの状況で簡単な操作をご紹介しました。これらを学び運用に慣れることは以降にご紹介するWeb開発においてとても重要になります。

results matching ""

    No results matching ""