これは ZOZO Advent Calendar 2022 カレンダー Vol.4 の 7 日目の記事です。
以下、Goを使用する前提で話を進めています。
何が起きたのか
Appleログインを実装する過程で、Appleから送られてきたIDトークンを構造体にパースしたら*json.UnmarshalTypeError
になっちゃいました。
原因
以下の構造体にパースするつもりでしたが、取得したIDトークンのemail_verified
がstring型だったのでパースできなかったわけですね。
type Profile struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
じゃあEmailVerifiedフィールドをstringにすればいいだけじゃないか、解散解散!
って思われたそこのあなた、公式docを見るとそれだけではダメそうです。
developerページには以下のように記載されています。
email_verified
A string or Boolean value that indicates whether the service verifies the email. The value can either be a string ("true" or "false") or a Boolean (true or false). The system may not verify email addresses for Sign in with Apple at Work & School users, and this claim is "false" or false for those users.
つまり、email_verifierはサービスがemail検証をするのかどうかを表す値で、型は文字列かboolのどっちかが返ってくるよ
ということですね。
リクエスト時に型を指定するようなパラメータもないので、どちらの型が返ってくるかはデコードするまでのお楽しみ☆くらいに思っておいた方が良さそうです。
なんだこの嫌がらせみたいな仕様!?って思いましたが、こちらのやりとりを見てみるとどうやら仕様ではなくバグっぽいです。
Sign in with Apple
が提供されたのが2019/6/3
と比較的最近なので、こういった未解決のバグは少なくないのかもしれませんね。
対応策
email_verified
がstringでもboolでも受け取れるようにします。
参考までに、AppleのIDプロバイダーから返ってくるIDトークンのペイロードはこんな感じのものを想定しています。
{
"iss": "https://appleid.apple.com",
"sub": "022311.ofgbdho8y41irquxn5rr7fkytf773lh2.1765",
"aud": [
"test_client_id"
],
"exp": 4073714420,
"iat": 1453272436,
"nonce": "a0230255d9e5865e7fe4683944833e41f683427f31554824930751dedd9cddf0",
"email": "test@example.com",
"email_verified": true,
"at_hash": "SjjfaAWSdWEvSDfASCmonm",
"alg": "HS256"
}
json周りの処理を構造体のタグだけで解決できなさそうな時は、MarshalJSON / UnmarshalJSONを使って複雑な処理を実装できます。
今回はApple ID Serverから返ってきたIDトークンを構造体に正しくパースしたいので、json.UnmarshalJSONを追加してjson.Unmarshalをオーバーライドします。
func (p *Profile) UnmarshalJSON(b []byte) error {
// 新しい構造体を定義して、Profile構造体には直接パースしない
type profileBool struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
}
type profileString struct {
Email string `json:"email"`
EmailVerified string `json:"email_verified"`
}
var pb profileBool
err := json.Unmarshal(b, &pb)
// email_verifiedがstringだった場合、profileBoolではなくprofileStringにマッピングする
if _, ok := err.(*json.UnmarshalTypeError); ok {
var ps profileString
e := json.Unmarshal(b, &ps)
if e != nil {
return e
}
// stringからboolに変換する
verifiedBool, e := strconv.ParseBool(ps.EmailVerified)
if e != nil {
return e
}
// もとのProfileにマッピングする
p.Email = ps.Email
p.EmailVerified = verifiedBool
return nil
}
if err != nil {
return err
}
p.Email = pb.Email
p.EmailVerified = pb.EmailVerified
return nil
}
これでProfile構造体には必ずbool型のemail_verifiedがパースされるようになりました!🎉
おわりに
なんとも困ったバグですが、このバグのおかげでMarshalJSON / UnmarshalJSONの理解が深まったので良しとします。
MarshalJSON / UnmarshalJSONは他にも、いろんな日時のフォーマットをtime.Time型に変換する例をよく見ます。
いろいろ用途がありそうなので、頭の片隅に置いておくと役に立つことがあるかもしれません。
ではまた。