TL; DR
type User struct {
Name string `json:"name"`
}
func main() {
user := &User{Name: "Taro"}
b, _ := json.MarshalIndent(user, "", "ほげほげ")
fmt.Println(string(b))
}
!?
{
ほげほげ"name": "Taro"
}
はじめに
json.MarshalIndent
は、オブジェクトを指定したインデント付きでmarshalできる関数です。
user := &User{Name: "Taro", Friends: []string{"Hanako", "Jiro"}}
// 第2引数はprefix、第3引数はindent
b, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(b))
{
"name": "Taro",
"friends": [
"Hanako",
"Jiro"
]
}
ネストの深いJSONの場合、Marshalよりも人間にとって読みやすいです。
比較(Marshal)
func main() {
user := &User{Name: "Taro", Friends: []string{"Hanako", "Jiro"}}
b, _ := json.Marshal(user)
fmt.Println(string(b))
}
{"name":"Taro","friends":["Hanako","Jiro"]}
ところで、MarshalIndentはインデントとしてどんな文字が使えるのでしょうか?
...実は、任意の文字が指定可能です。
JSONとしてパースできないバイト列を生成する
空白文字以外をインデントに指定することで、不正なJSONを生成できてしまいます。
user := &User{Name: "Taro", Friends: []string{"Hanako", "Jiro"}}
b, err := json.MarshalIndent(user, "", "ほげほげ")
if err != nil {
fmt.Println(err) // エラーは起きない!
return
}
fmt.Println(string(b))
}
{
ほげほげ"name": "Taro",
ほげほげ"friends": [
ほげほげほげほげ"Hanako",
ほげほげほげほげ"Jiro"
ほげほげ]
}
当然Unmarshalには失敗します。
// ...
var decoded User
if err := json.Unmarshal(b, &decoded); err != nil {
fmt.Println(err)
return
}
invalid character 'ã' looking for beginning of object key string
さらに悪用:中身を書き換える
インデントにプロパティを書き込めば中身の改ざんも可能です!
user := &User{Name: "Taro", Age: 20}
// nameを上書きするようなインデントを設定
b, _ := json.MarshalIndent(user, "", `"name": "Cracker",`)
fmt.Println(string(b))
{
"name": "Cracker","name": "Taro",
"name": "Cracker","age": 20
}
var decoded User
_ = json.Unmarshal(b, &decoded) // {Name:Cracker Age:20}
おまけ: jsoniterでは?
json-iteratorは、encoding/json
と100%の互換性を謳うモジュールです。
コードに一切の変更を入れずに高速化ができるのが売りですが、果たして...
import (
"fmt"
jsoniter "github.com/json-iterator/go"
)
// このjsonオブジェクトを `encoding/json` と全く同じように使える
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
user := &User{Name: "Taro", Age: 20}
b, _ := json.MarshalIndent(user, "", `"name": "Cracker",`)
fmt.Println(string(b))
}
panic: indent can only be space
goroutine 1 [running]:
github.com/json-iterator/go.(*frozenConfig).MarshalIndent(0xc0001280a0, {0x4fc020, 0xc000110498}, {0x0?, 0x0?}, {0x527939, 0x12})
/tmp/gopath4028780210/pkg/mod/github.com/json-iterator/go@v1.1.12/config.go:315 +0x14e
main.main()
/tmp/sandbox399066789/prog.go:20 +0x78
互換性は100%ではありませんでした
...というのは冗談で、不正な文字を弾いてくれるjsoniterの仕様の方がありがたいです...
(ちなみに、タブ対応のPRも上がっているようです)
おわりに
以上、json.MarshalIntent
の挙動についての紹介でした。Goはなんでも安全に倒す印象が強かったので、今回の結果には驚きました。
エラーを返してほしいところですが、Go compatibilityの都合で1系の間は今の仕様を変えられないでしょう。インデントを扱う場合はテストを入念にしておきたいところです。