Software engineering from east direction

六本木一丁目で働くソフトウェアエンジニアのブログ

生JSONを扱うのにちょっと便利な json.RawMessage と観測した実例

TL;DR

  • JSONを扱う際に、標準のjsonパッケージでは、 RawMessage という型が用意されている
  • JSON構造の文字列を取り出して、なんらか整形して出力、といったユースケースにちょっと便利

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

godoc.org

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
}

発想としては、以下のブログ記事内の

engineering.linecorp.com

ですので、SDKではUnmarshal用の別typeを用意し、その中でまずは type だけ先に読み取り、それから改めて型を決定してUnmarshalする、という方法を取りました。

という方法と類似していると見れそうです(と、@budougumi0617さんに教えていただきました)。

このようなケースがひとつ、 json.RawMessage の活用例としてあげられそうです。

mkr/monitors.go at ef0237b6e6c18cab2d9ce3d0fd34e166076f5fb0 · mackerelio/mkr · GitHub

f:id:khigashigashi:20200623021642j:plain:w320

Fin