生JSONを扱うのにちょっと便利な json.RawMessage と観測した実例
TL;DR
json.RawMessage
生JSONを扱う際に、標準のjsonパッケージでは、 RawMessage という型が用意されている。
// RawMessage is a raw encoded JSON value. // It implements Marshaler and Unmarshaler and can // be used to delay JSON decoding or precompute a JSON encoding. type RawMessage []byte
RawMessage is a raw encoded JSON value. It implements Marshaler and Unmarshaler and can be used to delay JSON decoding or precompute a JSON encoding.
上記の説明にある通り、 RawMessage は、 json.Marshaler interface と json.Unmarshaler interface を実装しているため、 JSON decode / encode に用いることが出来る。
https://godoc.org/encoding/json#Marshaler
type Marshaler interface { MarshalJSON() ([]byte, error) }
https://godoc.org/encoding/json#Unmarshaler
type Unmarshaler interface { UnmarshalJSON([]byte) error }
Marshal
たとえば、次のようにJSONを受け取り、その後それを json.MarshalIndent()
で整形すると言ったことも可能。
package main import ( "encoding/json" "fmt" "log" "os" ) func main() { m := json.RawMessage(`{"hoge": false, "huga": "piyo"}`) c := struct { FlexJson *json.RawMessage `json:"json"` }{FlexJson: &m} md, err := json.MarshalIndent(&c, "", "\t") if err != nil { log.Fatalf("error: %#v\n", err) } fmt.Fprint(os.Stdout, string(md)) }
https://play.golang.org/p/FFyY4fS89Du
ここで、JSON形式として破綻している構造の文字列だった場合は、json.MarshalIndent()
にて、json.SyntaxError
が返答される。
error: &json.SyntaxError{msg:"invalid character ']' after object key:value pair", Offset:88}
https://godoc.org/encoding/json#SyntaxError
// A SyntaxError is a description of a JSON syntax error. type SyntaxError struct { msg string // description of error Offset int64 // error occurred after reading Offset bytes } func (e *SyntaxError) Error() string { return e.msg }
Unmarshal
たとえば、配列形式の中に異なる構造のJSON Objectsがある場合、次のように、Unmarshalしてコード内で処理することも出来る。
package main import ( "encoding/json" "fmt" "log" "os" ) func main() { res := []byte(`{ "field1": [ {"hoge": false, "huga": "piyo"}, {"ponyo": 1, "piyo": false} ] }`) type c struct { Field1 []json.RawMessage `json:"field1"` } var cc c if err := json.Unmarshal(res, &cc); err != nil { log.Fatalf("error: %#v\n", err) } for _, v := range cc.Field1 { var dst interface{} if err := json.Unmarshal(v, &dst); err != nil { log.Fatalf("error: %#v\n", err) } fmt.Fprintf(os.Stdout, "%#v\n", dst) } }
https://play.golang.org/p/MsJFzMoLmKc
OSS内での使用例
json.RawMessageの現実世界で活用されているOSSの例として、mackerelio/mkr内で利用されています。
具体的には、mkr monitors pull
というコマンドを使用した際に、Mackerelサービスに設定されている監視設定をローカルに落としてくることが可能なのですが、その際の処理に json.RawMessage が活用されていました。
func decodeMonitors(r io.Reader) ([]mackerel.Monitor, error) { var data struct { Monitors []json.RawMessage `json:"monitors"` } if err := json.NewDecoder(r).Decode(&data); err != nil { return nil, err } ms := make([]mackerel.Monitor, 0, len(data.Monitors)) for _, rawmes := range data.Monitors { m, err := decodeMonitor(rawmes) if err != nil { return nil, err } ms = append(ms, m) } return ms, nil }
https://github.com/mackerelio/mkr/blob/ef0237b6e6c18cab2d9ce3d0fd34e166076f5fb0/monitors.go#L109
ここでの処理は、APIレスポンス内にある monitors キー以下に、JSON Objectの配列が含まれているのを、[]json.RawMessage
と定義することで取り出しています。
json.RawMessage
を利用した利点として、これ配列内でさらにJSON Objectをdecodeして、中のtype
キーの情報を取得、その内容によってunmarshalする構造を切り替えるということをしています。
func decodeMonitor(mes json.RawMessage) (mackerel.Monitor, error) { var typeData struct { Type string `json:"type"` } if err := json.Unmarshal(mes, &typeData); err != nil { return nil, err } var m mackerel.Monitor switch typeData.Type { case "connectivity": m = &mackerel.MonitorConnectivity{} case "host": m = &mackerel.MonitorHostMetric{} case "service": m = &mackerel.MonitorServiceMetric{} case "external": m = &mackerel.MonitorExternalHTTP{} case "expression": m = &mackerel.MonitorExpression{} case "anomalyDetection": m = &mackerel.MonitorAnomalyDetection{} } if err := json.Unmarshal(mes, m); err != nil { return nil, err } return m, nil }
発想としては、以下のブログ記事内の
ですので、SDKではUnmarshal用の別typeを用意し、その中でまずは type だけ先に読み取り、それから改めて型を決定してUnmarshalする、という方法を取りました。
という方法と類似していると見れそうです(と、@budougumi0617さんに教えていただきました)。
このようなケースがひとつ、 json.RawMessage の活用例としてあげられそうです。
mkr/monitors.go at ef0237b6e6c18cab2d9ce3d0fd34e166076f5fb0 · mackerelio/mkr · GitHub
Fin