go の json 挙動を色々と試したのでそれをまとめます。
json への変換の仕方
まずは、基本的なjsonへの変換の仕方です。 最も簡単な方法としては、以下のような感じで変換を行ないます。
import "encoding/json"
//...
jsonout, err := json.Marshal(対象のオブジェクト)
このやり方は分かりやすいですが、実際は以下のような形で変換することが多いと思います。
encoder := json.NewEncoder(jsonの書きこみ先のio.Writer)
err := encoder.Encode(対象のオブジェクト)
上記のやり方だと、 io.Writer
のインターフェイスであれば何でもいけるので、ファイル(os.File)に書きこんだり、httpのレスポンス(http.ResponseWriter)に書きこんだり、バッファ(bufio.Writer)に書きこんだりと自由度が高いです。
基本的な出力
次に基本的な構造体の出力を見ます。 以下のような普通の構造体の場合のjsonの出力内容は以下のようになります。
func main() {
type Sample struct {
IDString string
}
st := Sample{IDString: "xxfff"}
out, _ := json.Marshal(st)
fmt.Println(string(out)) // []byte型なのでstringに変換
// Output:
// {"IDString":"xxfff"}
}
構造体がそのままjsonに変換されるイメージです。直感的です。しかし、jsonの命名規則を考えると実用的かと言われるかと微妙です。 多くの人は json のキー名はローワーキャメルで、スネークケースにすることが多いと思います。そのためには、キー名を変更する必要があります。キー名を変更するには tag 文字を使用して変更します。 キー名を変更したケースとしては、以下のようになります。
func main() {
type Sample struct {
IDString string `json:"id_string"`
}
st := Sample{IDString: "xxfff"}
out, _ := json.Marshal(st)
fmt.Println(string(out)) // []byte型なのでstringに変換
// Output:
// {"id_string":"xxfff"}
}
いちいちタグ文字を入れるのが面倒と思う人は多いです。しかし、公式のjsonライブラリにはいい感じのオプションはないため、ツールを使ったりして、みなさん頑張っています。
jsonタグについて
json タグのフォーマットは以下のようになります。
`(...) json:"[<key>][,<flag1>[,<flag2>]]" (...)`
上記のように、json タグはキー名を変える以外にも指定できるものがあります。指定できる項目としては以下です。
- キー名
- omitempty
- string
キー名
この項目はその名の通り json の キー名のフィールドになります。後述しますが、キー名の指定の中でも最も優先度の高いものとなります。 -
を指定した場合は、そのフィールドをスキップすることになります。 何も書かず,
のみの場合は、通常通り構造体のフィールドの名前がそのまま使われます。
omitempty
この項目を指定した場合は、値がゼロ値のさいにスキップされます。go 言語の場合は、初期値がゼロ値として扱われるため、ポインタ型以外の型ではこの項目は使わないかなって思います。 イメージとしては以下のような感じです。
func main() {
type Sample struct {
ID string `json:",omitempty"`
Pointer *string `json:",omitempty"`
}
s := Sample{ID: "", Pointer: nil}
out, _ := json.Marshal(s)
fmt.Println(string(out)) // []byte型なのでstringに変換
// Output:
// {}
}
string
この項目は個人的に結構特殊な項目で、値をstring型に変更します。対応しているのは以下の組み込み型となっています。
- string
- byte
- rune
- int
- int8
- int16
- int32
- int64
- uint
- uint8
- uint16
- uint32
- uint64
- float32
- float64
- bool
- uintptr
また、string型も、string型に直されるため、 "
がエスケープされて出力されます。
func main() {
type Sample struct {
ID string `json:",string"`
}
s := Sample{ID: "xxffid"}
out, _ := json.Marshal(s)
fmt.Println(string(out)) // []byte型なのでstringに変換
// Output:
// {"ID":"\"xxffid\""}
}
出力順
出力順は構造体のフィールドの順番と同様になります。しかし、map のような順番が定まっていないものは、キー名のアルファベット順にソートがかけられます。何でこの順番と思いましたが、テストのしやすさを考えるとそんなもんかなぁという気持ちになります。
func main() {
s := map[string]string{
"cup": "one",
"apple": "two",
"banana": "three",
}
out, _ := json.Marshal(s)
fmt.Println(string(out)) // []byte型なのでstringに変換
// Output:
// {"apple":"two","banana":"three","cup":"one"}
}
埋め込み型
go 言語の構造体には埋め込み型が可能です。この型はフィールド名を指定ない型のためキー名も存在しません。そのため、基本的には埋め込まれた構造体のフィールドが展開されて出力されます。
func main() {
type Emb struct {
Content string
}
type Sample struct {
Emb
}
s := Sample{Emb: Emb{Content: "string"}}
out, _ := json.Marshal(s)
fmt.Println(string(out)) // []byte型なのでstringに変換
// Output: {"Content":"string"}
}
基本的と言ったように勿論例外があります。go言語では配列に対して型宣言することが可能です。この場合、展開を行なうと、配列が展開されることになるため、キーの名前が分かりません。そのため、配列の埋め込み型の場合は例外的に、型名がキー名となります。
func main() {
type Emb []string
type Sample struct {
Emb
}
s := Sample{Emb: Emb{"content1", "content2"}}
out, _ := json.Marshal(s)
fmt.Println(string(out)) // []byte型なのでstringに変換
// Output:
// {"Emb":["content1","content2"]}
}
キー名の優先順位
これまでの説明で、キー名は様々な要因で決まることがわかります。しかし、それではキーの衝突が起こってしまいます。そのため、goのjson変換には優先順位が定められています。 優先順位は以下で示すものです。
- tag で指定したキー名
- 通常のフィールド名
- 埋め込み型(埋め込み型のフィールドにも同じ優先順位が適用されます)
もし同じ優先度のものが複数あった場合は、どのキーの値も出力はされませんし、エラーも出力されません。埋め込み型を展開するさいなどに、気をつけないと嵌りそうな挙動です。
宣伝
色々と挙動がありますが、全部覚えるのも一々考えるのも面倒という人は多いと思います。そこで、構造体がどのような json を出力するか調べるツールを作成しました。 https://github.com/komem3/stout
使い方としては、以下のようにファイルパスと、構造体名を指定するだけです。
stout -path ./define.go SimpleStruct
この宣伝のために書いたのでした。