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^)/