LoginSignup
1
0

More than 1 year has passed since last update.

json.MarshalIndentはJSONとしての正当性を保証しない

Posted at

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系の間は今の仕様を変えられないでしょう。インデントを扱う場合はテストを入念にしておきたいところです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0