Go
golang
JSON

Go言語のjson.Marshal()でゆるふわにjsonを使う。

巷のGoのjsonについてのブログはjson.Unmarshal()の話だらけだが・・・。

標準パッケのみでゆるふわに使いたいのであればjson.Marshal()も重要なのに、
そういう説明があるブログがあまりないので書いてみました。
使い所としては、サーバサイドでDBの情報を引っ張ってきてjsonを投げ返す時とかクライアントとしてGoでPOSTする時ですかね。

今日の主役はこいつら

標準パッケージのencording/json。
https://github.com/golang/go/blob/master/src/encoding/json/encode.go

  • func Marshal(v interface{}) ([]byte, error)
  • func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

デモ

package main

import "fmt"
import "encoding/json"

func main() {
  m1 := map[string]interface{}{
    "string":"dayo", 
    "int":1, 
    "friends":true,
    "null":nil,
    "iary":[]int{10, 20, 30, 30, 4000},
    "sary":[]string{"stringAry", "dayo"},
    "inary":[]interface{}{10, false ,nil},
    "map":map[string]interface{}{"int":1},
    "mapmap":map[string]interface{}{"map":map[string]interface{}{"nemui":false}},
    "dup":map[string]interface{}{"stringAry":[]string{"string", "dayo"}},
  }

  m2 := map[string]interface{}{
    "orange":1,
    "banana":2,
    "apple":3,
  }

  var m2Ary []map[string]interface{}
  m2Ary = append(m2Ary, m2)
  m2Ary = append(m2Ary, m2)

  s, _ := json.Marshal(m1)
  fmt.Println("m1:\n")
  fmt.Println(string(s), "\n")
  fmt.Println("m1(indented):\n")
  i, _ := json.MarshalIndent(m1, "", "   ")
  fmt.Println(string(i), "\n")
  fmt.Println("m2array:\n")
  a, _ := json.MarshalIndent(m2Ary, "", "   ")
  fmt.Println(string(a), "\n")
}

map[string]interface{}にjsonっぽく突っ込むだけの圧倒的『ゆるふわ感』。
・・・これ動くの?と思う人もいるかもしれないが、
侮るなかれ、これがjson.Marshal()のチカラだ!

$ go run main.go
m1:

{"dup":{"stringAry":["string","dayo"]},"friends":true,"iary":[10,20,30,30,4000],"inary":[10,false,null],"int":1,"map":{"int":1},"mapmap":{"map":{"nemui":false}},"null":null,"sary":["stringAry","dayo"],"string":"dayo"} 

m1(indented):

{
   "dup": {
      "stringAry": [
         "string",
         "dayo"
      ]
   },
   "friends": true,
   "iary": [
      10,
      20,
      30,
      30,
      4000
   ],
   "inary": [
      10,
      false,
      null
   ],
   "int": 1,
   "map": {
      "int": 1
   },
   "mapmap": {
      "map": {
         "nemui": false
      }
   },
   "null": null,
   "sary": [
      "stringAry",
      "dayo"
   ],
   "string": "dayo"
} 

m2array:

[
   {
      "apple": 3,
      "banana": 2,
      "orange": 1
   },
   {
      "apple": 3,
      "banana": 2,
      "orange": 1
   }
] 

\キャーステキー!/
驚くことに、ちゃんとnilもjsonに併せてnullになってるとか、
mapのvalueを[]interface{}型でぶっこんでも、
としてぶっこんでも動くとか芸が細かい。
ちなみに、どうやらjsonのkeyをa-zでソートしているみたい。でもこれは(ry

まぁ何にせよ、知っておくべき使い方かと。

余談:引数に何を入れれば良いか。

どう考えてもmap[string]interaface{}なのだけれど。

まったくもってゆるふわな話ではないし、
興味ない人は見出しを見よ、結論はそれだ!

json.Marshal関数の引数は本来interface{}だけど何でも入るのか?
(本当はソースをだらだら解説書いてたけど、encoderの動きが奇々怪々な動きをしていて流石に全部追ってらんないので略しました)

これとか見るとjson.Marshal関数が何をやっているか一目瞭然のはず。

package main

import "fmt"
import "encoding/json"

func main() {
  test1, err := json.Marshal(1)
  fmt.Println(string(test1), err)  //->1 <nil>
  test2, err := json.Marshal("hello")
  fmt.Println(string(test2), err)  //->"hello" <nil>
  test3, err := json.Marshal(100.00000)
  fmt.Println(string(test3), err)  //->100 <nil>
  test4, err := json.Marshal(nil)
  fmt.Println(string(test4), err)  //->null <nil>
  test5, err := json.Marshal([]uint32{200, 200, 400})
  fmt.Println(string(test5), err)  //->[200,200,400] <nil>
}

なんと、非参照型や配列のみも引数としてOK笑
ソースを見れば分かるのだけどmap等の参照型だった場合再帰処理をかますだけなので、map以外の非参照型を入れた場合エラーではなくjsonのvalueに入るべきjson形式の文字列にパースするという働きをする分かる(nilがnullに、helloも"hello"に)。

まぁなかなかAPIとしてこの使い方はしないとは思う笑

しかしやはりmapのエンコーダーは違った動きをする。

package main

import "fmt"
import "encoding/json"

func main() {
  test1, err := json.Marshal(map[string]string{"hello":"world"})
  fmt.Println("test1: ", string(test1), err)
  test2, err := json.Marshal(map[string]interface{}{"hello":"world"})
  fmt.Println("test2: ", string(test2), err)
  test3, err := json.Marshal(map[int]string{10:"world"})
  fmt.Println("test3: ", string(test3), err)
  test4, err := json.Marshal(map[uint32]string{65000:"world"})
  fmt.Println("test4: ", string(test4), err)
  test5, err := json.Marshal(map[bool]string{false:"world"})
  fmt.Println("test5: ", string(test5), err)
  test6, err := json.Marshal(map[interface{}]string{nil:"world"})
  fmt.Println("test6: ", string(test6), err)
}
go run main.go
test1:  {"hello":"world"} <nil>
test2:  {"hello":"world"} <nil>
test3:  {"10":"world"} <nil>
test4:  {"65000":"world"} <nil>
test5:   json: unsupported type: map[bool]string
test6:   json: unsupported type: map[interface {}]string

見れば分かる通り、keyに何を入れようがjson形式でのstring型になっているのが分かる。まぁなんか当然だよなといった感じ。
さらに、keyにboolとかinterface{}とか意味がわからないものを入れるとエラーを吐く仕組み。この時点でもう引数はどう考えてもmap[string]interaface{}でしょうが、まぁ一応私もgopherを名乗るからにはソースにて確認する。
https://github.com/golang/go/blob/master/src/encoding/json/encode.go

func newMapEncoder(t reflect.Type) encoderFunc {
    switch t.Key().Kind() {
    case reflect.String,
        reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
        reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
    default:
        if !t.Key().Implements(textMarshalerType) {
            return unsupportedTypeEncoder
        }
    }
    me := &mapEncoder{typeEncoder(t.Elem())}
    return me.encode
}
type mapEncoder struct {
    elemEnc encoderFunc
}

func (me *mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
    if v.IsNil() {
        e.WriteString("null")
        return
    }
    e.WriteByte('{')

    // Extract and sort the keys.
    keys := v.MapKeys()
    sv := make([]reflectWithString, len(keys))
    for i, v := range keys {
        sv[i].v = v
        if err := sv[i].resolve(); err != nil {
            e.error(&MarshalerError{v.Type(), err})
        }
    }
    sort.Slice(sv, func(i, j int) bool { return sv[i].s < sv[j].s })

    for i, kv := range sv {
        if i > 0 {
            e.WriteByte(',')
        }
        e.string(kv.s, opts.escapeHTML)
        e.WriteByte(':')
        me.elemEnc(e, v.MapIndex(kv.v), opts)
    }
    e.WriteByte('}')
}

まぁ結論func newMapEncoder(t reflect.Type) encoderFuncでふるいにかけて、
func (me *mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts)でjson形式の文字列にパースしてるってオチ。

さっきの私が書いたスニペットでエラーが出たやつらは、t.Key().Kind()の結果、

reflect.String,
reflect.Int, 
reflect.Int8, 
reflect.Int16, 
reflect.Int32, 
reflect.Int64,
reflect.Uint, 
reflect.Uint8,
reflect.Uint16, 
reflect.Uint32, 
reflect.Uint64, 
reflect.Uintptr

のいずれかの型じゃなかったので、エラーとなったみたいですね。
(えっuintptr型OKなのかよww)
なのでこうなる。

package main

import "fmt"
import "reflect"
import "encoding/json"

func main() {
  var a1 interface{}
  a1 = map[string]string{"hello":"world"}
  b1 := reflect.ValueOf(a1)
  c1 := b1.Type()
  fmt.Println(c1.Key().Kind())  //->string

  var a2 interface{}
  prept := "hello"
  pt := reflect.ValueOf(&prept).Pointer()
  a2  = map[uintptr]string{pt:"world"}
  b2 := reflect.ValueOf(a2)
  c2 := b2.Type()
  fmt.Println(c2.Key().Kind())  //->uintptr

  var a3 interface{}
  a3 = map[float64]string{100.0000000000:"world"}
  b3 := reflect.ValueOf(a3)
  c3 := b3.Type()
  fmt.Println(c3.Key().Kind())  //->float64

  test1, err := json.Marshal(a1)
  fmt.Println(string(test1), err)  //->{"hello":"world"} <nil>

  test2, err := json.Marshal(a2)
  fmt.Println(string(test2), err)  //->{"842350502608":"world"} <nil>

  test3, err := json.Marshal(a3)
  fmt.Println(string(test3), err)  //-> json: unsupported type: map[float64]string
}

うむ、まぁ間違いなさそう。

結論、引数はmap[string]interface{}がオススメです。