Swift4
から Codableが導入されましたね。
Codable
が解決してくれた問題の一つに JSON
を通してデータのやり取りをするようなアプリにおいて定義してる構造体と 対応するJSON
のマッピングが楽になりました。
僕が開発しているアプリでもサーバーとのデータのやり取りは JSON
を使用していました。
Swift4
が使えるようになってからは積極的に Codable
を使っています。
それ以前、Swift3まではObjectMapperというライブラリを好んで使っていました。
もともとObjectMapper
を使っていて、その考え方でCodable
を使っていたら危険だな、と思った話をしていこうと思います。
今回そう思うきっかけになったのが CodableでEnumの配列をデコードするとき
だったのでそれを例にして書いていこうと思います。
Enumの配列をデコードしてみる
下記のようなCodable
に準拠した struct
とenum
があったとして、それぞれの関係も記します。
enum SocialAccountType: String, Codable {
case twitter
case facebook
}
struct User: Codable {
let id: Int
let accounts: [SocialAccountType]
}
Decode
をすると下記のような結果になります
let response = """
{
"id": 1,
"accounts": ["facebook", "twitter"]
}
"""
let json = response.data(using: .utf8)!
let decoded = try! JSONDecoder().decode(User.self, from: json)
print(decoded)
// User(id: 1, accounts: [SocialAccountType.facebook, SocialAccountType.twitter])
ちゃんとDecode
できてますね。
次はObjectMapper
ではどうなるか見ていきましょう。
宣言がvar
になったり Optional
になったり
enum SocialAccountType: String {
case twitter
case facebook
}
struct User: Mappable {
var id: Int?
var accounts: [SocialAccountType]?
init?(map: Map) {
id <- map["id"]
accounts <- map["accounts"]
}
mutating func mapping(map: Map) { }
}
同じように JSON
の文字列から mapping
して User
のインスタンスを作りたいと思います
let user = Mapper<User>().map(JSONString: response)!
print(user)
// User(id: Optional(1), accounts: Optional([SocialAccountType.facebook, SocialAccountType.twitter]))
Optional
が付いてますがちゃんと mapping
ができていると思います。
本題
さて、仮に上のロジックでアプリがリリースされている場合を想定しましょう。
そして、SocialAccountType
に一つ要素を足したくなたっときの場合を想定します。
ここではqiita
という要素をSocialAccountType
に足していきます。
しかし、Swift
ファイルの方は編集しないです。編集するのは JSON
の文字列の方です。
let response = """
{
"id": 1,
"accounts": ["facebook", "twitter", "qiita"]
}
"""
こうですね
さて、この場合で先ほどのCodable
, ObjectMapper
両パターンで実際に動かすとどうなるでしょう
順序を変えて ObjectMapper
から
let user = Mapper<User>().map(JSONString: response)!
print(user)
// User(id: Optional(1), accounts: Optional([SocialAccountType.facebook, SocialAccountType.twitter]))
先ほどと実行結果がかわあず
さて次に Codable
をやっていきます。
let json = response.data(using: .utf8)!
let decoded = try JSONDecoder().decode(User.self, from: json)
print("decoded value: \(decoded)")
Playground
上では何も出力されませんでした。
これは try JSONDecoder().decode(User.self, from: json)
で例外が起きたためです。
catch
して print
して見ましょう。
let json = response.data(using: .utf8)!
do {
let decoded = try JSONDecoder().decode(User.self, from: json)
print("decoded value: \(decoded)")
} catch {
print("error: \(error.localizedDescription)")
// error: The data couldn’t be read because it isn’t in the correct format.
}
フォーマット違うから読み込めなかったよ。って怒られましたね。
JSON
から Enum
の配列を Decode
するようなときには
ObjectMapper
の時には変換できなかったものは無視する(ここら辺
Codable
(正確にはDecodable
)の場合は例外を発する(ここだ!って場所見つけられなかった)
という結果になりました。
どういう時に危ない?
さて、挙動の違いがわかったところでどういうシチュエーションで危なそうかちょっと触れてまとめたいと思います。
危ないというよりは思い込みにより予期せぬことが起き得る可能性があります。
例えばJSON
でサーバーとのやり取りをしているアプリで想定して、少し前まで ObjectMapper
を使っていましたが、Codable
に書き換えました。問題なく動作していて、なんだCodable
、 ObjectMapper
と同じような使い方できるじゃねえか、いいやつだな。おい。と思ってました。 そんな時にqiita
という要素を足すことになりました。ObjectMapper
の感覚では存在しないcase
は無視されて、accounts
にはそのアプリのバージョンで.swift
に書かれている case
の要素分の配列が作られるはずだから特に新しい要素が増えることに対して個別に対応する必要もないし、ただ表示するような用途しか使ってないから大丈夫大丈夫。と先入観がある可能性が大いにあり得ます。こんな状態でCodable
を使った場合は先ほどの結果からわかるように例外を発するのでアプリが何かしら予期していなかった動きになる可能性があります。
終わりに
具体的に何を選択するのか、アプリのバージョニングによる差分はどうやって埋めるのがいいか、といったベストプラクティスは持ち合わせていないので、ここでは書きません。むしろどなたか教えてください。
他に Codable
の癖みたいなのもまだ把握しきっていないので
CodableでEnumの配列をデコードするときに注意すること
として書いてみました。
ここまでの比較で ObjectMapper
を例にしてあげましたが、ObjectMapper
じゃなくても Codable
との差異がある可能性は大いにあるし、どっちがいいか悪いかといった記事では無いことをご理解いただけると幸いです。
Codable
と 今まで使っていた ORM
の機能の差はあるかもしれない。と先入観持たずに取り組みましょう。という記事でした。
おしまい \(^o^)/