例えばこんな構造体が定義されているとして
type User struct { Name string `json:"name"` Status int `json:"status"` }
この構造体をJSONからUnmarshalしてマッピングする際に、以下のような要件があるとする
status
の取りうる範囲は1,2だけ、かつそれぞれACTIVE
,INACTIVE
という定数にマッピングしたい。- 1,2以外の数値が来たらエラーにしたい。
status
フィールドがないときもエラーにしたい。
User構造体を受け取ってバリデーションをする関数を作ってもよいが、以下のようにStatus型を新しく作り、それにUnmarshallerインターフェースを実装させるとグッとスマートにできる。
type User struct { Name string `json:"name"` Status Status `json:"status"` // int型からStatus型に変更 }
Unmarshallerインターフェースを実装したStatus型
Unmarshallerインターフェースは以下のような定義になっている
type Unmarshaler interface { UnmarshalJSON([]byte) error }
JSONのUnmarshalをする際、マッピング対象のフィールドにUnmarshallerインターフェースを実装したものがあると、そのフィールドへのマッピングの際に自動的に実装されたUnmarshalJSONメソッドが呼ばれるようになる。
つまり、Status型にこのUnmarshallerインターフェースを実装することで、User型がUnmarshalされる際に自動でStatus型に実装された任意のマッピング処理およびバリデーション(!)行えるようになるというわけだ。
Unmarshallerインターフェースを用いて要件を実装したStatus型は以下になる。
type Status int const ( ACTIVE Status = iota + 1 INACTIVE ) // Unmashaller func (status *Status) UnmarshalJSON(data []byte) error { var value int if err := json.Unmarshal(data, &value); err != nil { return fmt.Errorf("Failed decoding status: %s", err.Error()) } switch value { case 1: *status = ACTIVE case 2: *status = INACTIVE default: return fmt.Errorf("Invalid status: %d", value) } return nil }
statusの取りうる範囲をバリデーションできているか見てみる
上の実装を終えた上で、Status型をフィールドに持つUser型をJSONからマッピングしてみるとこうなる
func main() { src := `{ "name": "Jonathan", "status": 3 }` var user User if err := json.Unmarshal([]byte(src), &user); err != nil { panic(err) // panic: Invalid status: 3 } // ... }
status
に3が与えられているのはUnmarshal時にマッピングエラーになる! すばらしい
statusフィールドが存在しない場合にどうするか
実はこれだけではまだ、以下のようにstatus型が抜け落ちているJSONのマッピングに対するエラー検知ができていない
{ "name": "Jonathan" }
上のJSONをマッピングするとどういう挙動になるかというと以下のようになる。
func main() { src := `{ "name": "Jonathan" }` var user User if err := json.Unmarshal([]byte(src), &user); err != nil { panic(err) } fmt.Printf("%#v\n", user) // main.User{Name:"Jonathan", Status:0} }
つまりint型の初期値である0が代入される。
自分の場合は以下の IsEmpty
のようなレシーバメソッドを生やして、仮にUnmarshalが成功したとしても、必ず直後にIsEmpty
を事前条件チェックとして呼ぶようにしているが、若干微妙ではある。
type Status int const ( ACTIVE Status = iota + 1 INACTIVE ) func (status Status) IsEmpty() bool { return status == 0 } // ... func main() { src := `{ "name": "Jonathan" }` var user User if err := json.Unmarshal([]byte(src), &user); err != nil { panic(err) } if user.Status.IsEmpty() { panic("status is empty") } fmt.Printf("%#v\n", user) }
本当はUnmarshalJSONメソッドの中でどうにかしたいが、Unmarshal対象のフィールドがない場合には当たり前だが呼ばれない。ここはちょっと苦しい。