goでJSONのunmarshal時のrequiredなfieldの扱いについて
TL;DR
- goであんまりfieldにpointerを使いたくない
- required,unrequiredを表現するときにはpointerを使わざる負えない?
- unmarshal時に自分でチェックすればやれないこともない
はじめに
goのJSONへの対応について悩まされる事が多い。色々試行錯誤を繰り返したり何かのツールやライブラリでの捉え方を見てみたりはするのだけれど。これが良いという形を見出だせない。
何が問題なのかというと、goではpointerを多用するとnil dereference panicが起きやすいことで可能ならこれを避けたい(あとstruct自体をfieldにするとzero値で初期化できるのでテストなどで便利というような副作用もある)。一方でpointerを利用しなくてはrequiredのチェックができないということ。
似たような考えや思いは古くからあるらしく例えばqiitaのこの記事などは近い視点で書かれているように思う。
fieldのrequired/unrequired
例えばあるstructのUserがあったとしてnameをstringでageをintで定義するとする。
type User struct { Name string `json:"name"` Age int `json:"age"` }
ここでname,ageをrequiredにしたい。このstruct定義ではunmarshal時marshal時それぞれに問題を抱えている。
unmarshal時の問題
unmarshalというのは string([]byte) -> object
の場合の話。上の定義をそのまま利用してunmarshalした場合にはzero valueで初期化されたのかそれとも値が存在しなかったのかgo側の値になった後では判断が付かない(structの値生成時に対応するフィールドの初期値を与えなかった場合にはzero valueで初期化されるので)。
ageが存在していない場合
var user User s := `{"name": "foo"}` json.Unmarshal([]byte(s), &user) // main.User{Name: "foo", Age: 0}
ageに0の値が渡された場合
var user User s := `{"name": "foo", "age": 0}` json.Unmarshal([]byte(s), &user) // main.User{Name: "foo", Age: 0}
(同様の問題はもちろんstringでも起こり。この場合は空文字列を許容するかという話になる)
一方で型をpointerにしてしまえばnilかそうでないかでrequired,unrequiredを判別する事ができる。しかし取り回しがし辛い。
type User struct { Name *string `json:"name"` // required Age *int `json:"age"` // required }
(実際真面目にjsonschemaなりの仕様を忠実に再現しようとするとpointerになってしまう。完全にmodel用の表現と値の受け渡し用の表現を分けて変換関数を別途作るなどやっても良いがコストが大きい)
marshal時の問題
marshalというのは object -> string([]byte)
の場合の話。marshal時にも同様の問題は起きる。とは言え通常marshalといえばどこかに保存されたデータを取ってくる。そしてデータは一度validationを通り抜けたinputを元に作られる(はず)なので。壊れたデータがやってくる事は少ない。なのでmarshal時に初期値の有無が判別できなくても別に困らないのでは?という思いがある。
(goにおいて値を渡し忘れたかどうかを静的に判別する術が無いと言うのは問題ではあるのだけれど。それは永続化されたデータを取ってくるときの問題。のはず。複数の値の集計結果を保持したい場合の話で言えばちょっと状況が異なるかもしれない)
unmarshalのrequired/unrequiredの解釈
元の記事では何らかのライブラリ(当時では(?)jason, go-simplejson, go-scan)を使って書くという方法だったけれど。特にライブラリを使う必要は無く自前でUnmarshalJSONを定義すれば、文字列をparseするタイミングでrequired/unrequiredのチェックはできそうだった(依存は少ない方が良い)。
例えば以下の様な形で全部pointerのstructをlocalで定義してそちらで一度unmarshalしてから元の型に値を受け渡す形にする。
// UnmarshalJSON : func (u *User) UnmarshalJSON(b []byte) error { type userP struct { Name *string `json:"name"` // required Age *int `json:"age"` // required } var p userP if err := json.Unmarshal(b, &p); err != nil { return err } if p.Name == nil { return errors.Errorf("in User, name is not found data=%s", b) } u.Name = *p.Name if p.Age == nil { return errors.Errorf("in User, age is not found data=%s", b) } u.Age = *p.Age return nil }
このようにしてあげれば外の世界からJSON経由でやってきた場合にはrequired/unrequiredが正しくチェックされるようになる。
var user User s := `{'name': 'foo'}` json.Unmarshal([]byte(s), &user) // in User, age is not found data={"name": "foo"}
ネストした場合
ネストのときも同様で単にstruct毎にUnmarshalJSONを定義して上げれば良い(この時内部のネストされたstruct部分は単にコピーするだけで良い)。
// User : type User struct { Name string `json:"name"` Age int `json:"age"` Skills []Skill `json:"skills"` } // Skill : type Skill struct { Name string `json:"name"` } // UnmarshalJSON : func (u *User) UnmarshalJSON(b []byte) error { type userP struct { Name *string `json:"name"` Age *int `json:"age"` Skills *[]Skill `json:"skills"` } var p userP if err := json.Unmarshal(b, &p); err != nil { return err } if p.Name == nil { return errors.Errorf("in User, name is not found data=%s", b) } u.Name = *p.Name if p.Age == nil { return errors.Errorf("in User, age is not found data=%s", b) } u.Age = *p.Age // skillsは自身のUnmarshalJSONによりチェック済みの値が埋まっているので単に受け渡しをするだけで良い if p.Skills == nil { return errors.Errorf("in User, skills is not found data=%s", b) } u.Skills = *p.Skills return nil } // UnmarshalJSON : func (u *Skill) UnmarshalJSON(b []byte) error { type skillP struct { Name *string `json:"name"` } var p skillP if err := json.Unmarshal(b, &p); err != nil { return err } if p.Name == nil { return errors.Errorf("in Skill, name is not found data=%s", b) } u.Name = *p.Name }
:feet: ところで現代においては関数を手書きするというのはやってられないのでコード生成する仕組みをあっても(作っても)良いように思う。
型定義からpointerを無くすことは可能?
そんな悪者呼ばわりされるpointerによるフィールドの定義だけれど。これを無くすというのは不可能。
例えば再帰的な型の定義には絶対にpointerが必要になる(無限再帰が発生するので)。
type User struct { Name string `json:"name"` Age int `json:"age"` Skills []Skill `json:"skills"` Father User `json:"father"` Mother User `json:"mother"` }
あるいはjsonschemaでいうoneOf的なデータの取扱いもnilの方が自然(こちらについてはJSON側での表現をoneOfのそれにするか決める必要もありそう)。
type Item struct { Type string `json:"type"` // a or b A *A `json:"a,omitempty"` B *B `json:"b,omitempty"` }