1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift】JsonDecoderでEnumにデコードする時に該当Caseが無くてもエラーにしない

Last updated at Posted at 2025-02-23

概要

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もそれぞれエラーを投げずに正常に動作します。
強制アップデートのようなことをする必要もなく、拡張していけるのでいいかと思います

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?