概要
JsonDecoderでJsonからEnumへデコードする際、Enumに定義されていないcaseを処理する場合はそのままだとErrorを投げます。
これを避ける方法をまとめておきます。
正常系
食事のタイプとカロリーの記録をしたJsonがあるとします。
これをAPP内で扱えるように構造体にDecodeします。
import Foundation
// Jsonデータ
let mealJson = """
[
{
"type": "breakfast",
"calorie": 350,
"date": "2025-02-20T12:00:00Z"
}
]
""".data(using: .utf8)!
// デコード先の構造体
struct Meal: Codable {
var type: MealType
var calorie: Int
var date: Date
}
enum MealType: String, Codable {
case breakfast
case lunch
case dinner
}
// デコード処理
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let meals = try decoder.decode([Meal].self, from: mealJson)
meals.forEach { meal in
print(meal.type.rawValue) // breakfast
}
} catch {
print(error.localizedDescription)
}
mealJson
をJsonDecoderでデコードしました。
enum MealType
に定義されているbreakfastをJsonで受け取るような場合は正常にデコードされ、"breakfast"がprintされました。
定義していないcaseが返ってくると...
新しくsnack
というtypeを返したい場合がでてきました。
先ほどのコードのままJsonにsnackを追するとどうなるでしょう。
// "type": "snack"を追加
let mealJson = """
[
{
"type": "breakfast",
"calorie": 350,
"date": "2025-02-20T12:00:00Z"
},
{
"type": "snack",
"calorie": 150,
"date": "2025-02-20T15:00:00Z"
}
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let meals = try decoder.decode([Meal].self, from: mealJson)
meals.forEach { meal in
print(meal.type.rawValue)
}
} catch {
print(error.localizedDescription) // The data couldn’t be read because it isn’t in the correct format.
}
DecoderがErrorを投げ、エラー文がprintされました。
これ自体が悪いわけではありません。アプリが求める仕様次第では、エラーをキャッチして異常系としてエラーハンドリングすれば問題ない場合もあるでしょう。
しかし、このJsonをRemoteConfigのように外部から受け取っている場合はどうでしょう。
Jsonの値を更新し、アプリ側もそれを扱えるようにEnumにcaseを更新してアップデート...
としてしまうと、アップデート前のバージョンのアプリを使用しているユーザーも更新後のJsonの値を参照しますから、上記例のようにエラーを投げます。
本当は、
- 旧バージョンアプリ:snackを無視しそれ以外は正常に処理
- 新バージョンアプリ:snackを含めた全てのケースを正常に処理
が理想ですよね。
修正してみる
結論、未定義の値がきたときのデフォルトcaseとカスタムイニシャライザをEnumに定義します。
enum MealType: String, Codable {
case breakfast
case lunch
case dinner
case unknown // 未定義の値はこのcaseに収束させる
// カスタムイニシャライザ
init(from decoder: Decoder) throws {
let container = try? decoder.singleValueContainer()
let rawValue = (try? container?.decode(String.self)) ?? ""
// rawValueが空文字の場合.unknownにする
self = MealType(rawValue: rawValue) ?? .unknown
}
}
上記のように、するとMealType
に適合しない文字列が来た場合に.unknown
として処理するようになります。
この修正を受けて、Jsonのデコード処理はどうなるでしょうか。
// Enumにないsnackを返す
let mealJson = """
[
{
"type": "breakfast",
"calorie": 350,
"date": "2025-02-20T12:00:00Z"
},
{
"type": "snack",
"calorie": 350,
"date": "2025-02-20T12:00:00Z"
}
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let meals = try decoder.decode([Meal].self, from: mealJson)
meals.forEach { meal in
switch meal.type {
case .breakfast, .dinner, .lunch:
print (meal.type.rawValue) // 定義されている値はこちらで処理
case .unknown:
break // 未定義の値はこちらで処理(エラーにならない)
}
}
} catch {
print(error.localizedDescription)
}
このようにデコードエラーが起きなくなるため、Enumで定義された値と定義されてない値も処理できるようになりました。
おわりに
このように実装できると、旧バージョンAPPも新バージョンAPPもそれぞれエラーを投げずに正常に動作します。
強制アップデートのようなことをする必要もなく、拡張していけるのでいいかと思います