Goの構造体(struct)をJSONに変換(Marshal)するeasyjsonの使い方を解説

thumnail
2020-07-25

背景

業務で構造体で定義したlogをJSON形式で標準出力にParseする実装をする場面があった。そのときに今回紹介するeasyjsonというGoのパッケージを使用したので、その使い方を備忘録としてこの記事に残しておく。

easyjsonとは

JSONとGo Structを相互に変換できる

easyjsonというパッケージを使えば、GoのstructとJSONをMarshal/Unmarshalするためのコードを自動生成することができる。

Marshal/Unmarshal とは

marshalを直訳すると「元帥」という意味になる。プログラミングにおける意味はこれではない。プログラミングにおける意味として、あるプログラムのデータ形式を他のプログラムで動作可能な状態に変換することを指す。恐らく今回はこの意味がもっとも近いと思われる。つまりGoのstructからJSONに変換することをMarshalと言っているっぽい。

easyjsonをダウンロード

次のコマンドを叩けばeasyjsonをダウンロードできる。

go get -u github.com/mailru/easyjson/...

easyjsonを使ってみる

次のstructを定義したファイルを用意する。

$ cat hoge.go
package hoge

type Hoge1 struct {
        A string `json:"a"`
        B *Hoge2
}

type Hoge2 struct {
        C []int          `json:"c"`
        D map[string]int `json:"d"`
}

次のコマンドを実行するとGoのstructとJSONをMarshal/Unmarshalする為のファイルが自動生成される。

$ easyjson -all hoge.go

このコマンドを叩くとhoge_easyjsonというファイルが生成される。

$ cat hoge_easyjson

// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.

package hoge

import (
    json "encoding/json"
    easyjson "github.com/mailru/easyjson"
    jlexer "github.com/mailru/easyjson/jlexer"
    jwriter "github.com/mailru/easyjson/jwriter"
)

// suppress unused package warning
var (
    _ *json.RawMessage
    _ *jlexer.Lexer
    _ *jwriter.Writer
    _ easyjson.Marshaler
)

func easyjson154013cbDecodeTestProjectHoge(in *jlexer.Lexer, out *Hoge2) {
    isTopLevel := in.IsStart()
    if in.IsNull() {
        if isTopLevel {
            in.Consumed()
        }
        in.Skip()
        return
    }
    in.Delim('{')
    for !in.IsDelim('}') {
        key := in.UnsafeString()
        in.WantColon()
        if in.IsNull() {
            in.Skip()
            in.WantComma()
            continue
        }
        switch key {
        case "c":
            if in.IsNull() {
                in.Skip()
                out.C = nil
            } else {
                in.Delim('[')
                if out.C == nil {
                    if !in.IsDelim(']') {
                        out.C = make([]int, 0, 8)
                    } else {
                        out.C = []int{}
                    }
                } else {
                    out.C = (out.C)[:0]
                }
                for !in.IsDelim(']') {
                    var v1 int
                    v1 = int(in.Int())
                    out.C = append(out.C, v1)
                    in.WantComma()
                }
                in.Delim(']')
            }
        case "d":
            if in.IsNull() {
                in.Skip()
            } else {
                in.Delim('{')
                out.D = make(map[string]int)
                for !in.IsDelim('}') {
                    key := string(in.String())
                    in.WantColon()
                    var v2 int
                    v2 = int(in.Int())
                    (out.D)[key] = v2
                    in.WantComma()
                }
                in.Delim('}')
            }
        default:
            in.SkipRecursive()
        }
        in.WantComma()
    }
    in.Delim('}')
    if isTopLevel {
        in.Consumed()
    }
}
func easyjson154013cbEncodeTestProjectHoge(out *jwriter.Writer, in Hoge2) {
    out.RawByte('{')
    first := true
    _ = first
    {
        const prefix string = ",\"c\":"
        out.RawString(prefix[1:])
        if in.C == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
            out.RawString("null")
        } else {
            out.RawByte('[')
            for v3, v4 := range in.C {
                if v3 > 0 {
                    out.RawByte(',')
                }
                out.Int(int(v4))
            }
            out.RawByte(']')
        }
    }
    {
        const prefix string = ",\"d\":"
        out.RawString(prefix)
        if in.D == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 {
            out.RawString(`null`)
        } else {
            out.RawByte('{')
            v5First := true
            for v5Name, v5Value := range in.D {
                if v5First {
                    v5First = false
                } else {
                    out.RawByte(',')
                }
                out.String(string(v5Name))
                out.RawByte(':')
                out.Int(int(v5Value))
            }
            out.RawByte('}')
        }
    }
    out.RawByte('}')
}

// MarshalJSON supports json.Marshaler interface
func (v Hoge2) MarshalJSON() ([]byte, error) {
    w := jwriter.Writer{}
    easyjson154013cbEncodeTestProjectHoge(&w, v)
    return w.Buffer.BuildBytes(), w.Error
}

// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Hoge2) MarshalEasyJSON(w *jwriter.Writer) {
    easyjson154013cbEncodeTestProjectHoge(w, v)
}

// UnmarshalJSON supports json.Unmarshaler interface
func (v *Hoge2) UnmarshalJSON(data []byte) error {
    r := jlexer.Lexer{Data: data}
    easyjson154013cbDecodeTestProjectHoge(&r, v)
    return r.Error()
}

// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Hoge2) UnmarshalEasyJSON(l *jlexer.Lexer) {
    easyjson154013cbDecodeTestProjectHoge(l, v)
}
func easyjson154013cbDecodeTestProjectHoge1(in *jlexer.Lexer, out *Hoge1) {
    isTopLevel := in.IsStart()
    if in.IsNull() {
        if isTopLevel {
            in.Consumed()
        }
        in.Skip()
        return
    }
    in.Delim('{')
    for !in.IsDelim('}') {
        key := in.UnsafeString()
        in.WantColon()
        if in.IsNull() {
            in.Skip()
            in.WantComma()
            continue
        }
        switch key {
        case "a":
            out.A = string(in.String())
        case "B":
            if in.IsNull() {
                in.Skip()
                out.B = nil
            } else {
                if out.B == nil {
                    out.B = new(Hoge2)
                }
                (*out.B).UnmarshalEasyJSON(in)
            }
        default:
            in.SkipRecursive()
        }
        in.WantComma()
    }
    in.Delim('}')
    if isTopLevel {
        in.Consumed()
    }
}
func easyjson154013cbEncodeTestProjectHoge1(out *jwriter.Writer, in Hoge1) {
    out.RawByte('{')
    first := true
    _ = first
    {
        const prefix string = ",\"a\":"
        out.RawString(prefix[1:])
        out.String(string(in.A))
    }
    {
        const prefix string = ",\"B\":"
        out.RawString(prefix)
        if in.B == nil {
            out.RawString("null")
        } else {
            (*in.B).MarshalEasyJSON(out)
        }
    }
    out.RawByte('}')
}

// MarshalJSON supports json.Marshaler interface
func (v Hoge1) MarshalJSON() ([]byte, error) {
    w := jwriter.Writer{}
    easyjson154013cbEncodeTestProjectHoge1(&w, v)
    return w.Buffer.BuildBytes(), w.Error
}

// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Hoge1) MarshalEasyJSON(w *jwriter.Writer) {
    easyjson154013cbEncodeTestProjectHoge1(w, v)
}

// UnmarshalJSON supports json.Unmarshaler interface
func (v *Hoge1) UnmarshalJSON(data []byte) error {
    r := jlexer.Lexer{Data: data}
    easyjson154013cbDecodeTestProjectHoge1(&r, v)
    return r.Error()
}

// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Hoge1) UnmarshalEasyJSON(l *jlexer.Lexer) {
    easyjson154013cbDecodeTestProjectHoge1(l, v)
}

これでGoのstructをJSONに変換するプログラムが準備できた。では実際に変換してみる。やり方は簡単でJSONに変換したいstructをMarshalというメソッドの引数に指定すれば良い。

$ cat main.go

package main

import (
    "fmt"
    "test-project/hoge"

    easyjson "github.com/mailru/easyjson"
)

func main() {
    a := hoge.Hoge1{A: "hogeA", B: &hoge.Hoge2{
        C: []int{1, 2, 3},
    }}
    b, err := easyjson.Marshal(a)
    if err != nil {
        panic(err)
    }

    fmt.Println(a)         // {hogeA 0xc0001201e0}
    fmt.Println(string(b)) // {"a":"hoge string","B":{"c":[1,2,3],"d":null}}
}

MarshalにGoの構造体を渡すだけで、JSONに変換できていることがわかる。

細かいオプションの設定はREADMEを見れば良い。

https://github.com/mailru/easyjson

この記事内容とは関係ありませんが、筆者である僕が普段愛用しているPC周りのアイテムを別記事で紹介しているので、もし興味があればご覧になってください。

現在使用しているディスプレイ・ヘッドフォン・キーボードなどを紹介

KATUBLO

Copyright since 2018 KATUO All Rights Reserved.