記事

CodableのDecode問題|ResponseのNull値を効率的に処理する方法まとめ

Codableでのデコード時に直面するNull値問題を解決。無駄なinit decoderの再実装を避け、現実的な対応策で安定したデータ処理を実現します。

CodableのDecode問題|ResponseのNull値を効率的に処理する方法まとめ

本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。

記事一覧


実際の Codable 使用時に遭遇するデコード問題の事例まとめ(後編)

レスポンスのNullフィールドのデータを適切に処理するには、必ずしもinit decoderを再実装する必要はありません

Photo by [Zan](https://unsplash.com/@zanilic?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

Photo by Zan

はじめに

前回の「現実の Codable 使用時に直面する Decode の問題シナリオまとめ」に続き、開発が進む中で新たなシナリオと問題に直面しました。そこで本記事では、遭遇した状況や考察を引き続き記録し、後で振り返りやすくしています。

前篇では主に JSON String → Entity Object の Decodable マッピングを解決しました。Entity Object ができたら、プログラム内での受け渡しに使う Model Object や、データ表示ロジックを処理する View Model Object に変換できます。一方で、Entity を NSManagedObject に変換してローカルの Core Data に保存する必要があります

主な問題

私たちの曲の Entity 構造が以下のようだとします:

1
2
3
4
5
6
7
8
9
struct Song: Decodable {
    var id: Int
    var name: String?
    var file: String?
    var converImage: String?
    var likeCount: Int?
    var like: Bool?
    var length: Int?
}

APIのエンドポイントは必ずしも完全なデータフィールドを返すとは限らず(idだけは必ず返される)、id以外のフィールドはすべてOptionalです。例えば、曲の情報を取得する際は完全な構造が返されますが、曲の「お気に入り」や「いいね」を更新する場合は、idlikeCountlikeの3つの関連する変更フィールドのみが返されます。

私たちは API レスポンスのすべてのフィールドデータを Core Data に保存し、既にデータが存在する場合は変更されたフィールドのみを更新(インクリメンタルアップデート)したいと考えています。

しかしこの時、問題が発生しました:Codable Decode から Entity Object に変換すると、「データフィールドを nil に設定したい」か「レスポンスで渡されていない」かを区別できません

1
2
3
4
5
A Response:
{
  "id": 1,
  "file": null
}

A Response、B Response の file はどちらも null ですが、意味は異なります。A は file フィールドを null に設定して(元のデータをクリア)、B は他のデータを更新したいだけで、単に file フィールドを渡していません。

Swift コミュニティの開発者が date Strategy のような null Strategy を JSONDecoder に追加する提案 を出していますが、現時点では導入予定はありません。

解決策

前述の通り、私たちの構造は JSON String -> Entity Object -> NSManagedObject なので、Entity Object を受け取った時点で既にデコード済みの結果であり、操作できる生のデータはありません。ここで原始の JSON String を比較して操作することも可能ですが、そうするくらいなら Codable を使わない方が良いです。

まずは 前回 を参照して、Associated Value Enum を使って値を格納する方法について。

1
2
3
4
5
6
7
8
9
10
11
12
enum OptionalValue<T: Decodable>: Decodable {
    case null
    case value(T)
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode(T.self) {
            self = .value(value)
        } else {
            self = .null
        }
    }
}

ジェネリックを使用し、T は実際のデータフィールドの型です。.value(T) はデコードされた値を保持し、.null は値が null であることを示します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
struct Song: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case file
    }
    
    var id: Int
    var file: OptionalValue<String>?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        self.id = try container.decode(Int.self, forKey: .id)
        
        if container.contains(.file) {
            self.file = try container.decode(OptionalValue<String>.self, forKey: .file)
        } else {
            self.file = nil
        }
    }
}

var jsonData = """
{
    "id":1
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

jsonData = """
{
    "id":1,
    "file":null
}
""".data(using: .utf8)!
result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

jsonData = """
{
    "id":1,
    "file":\"https://test.com/m.mp3\"
}
""".data(using: .utf8)!
result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

例として、idfile の2つのデータフィールドだけに簡略化します。

Song Entity は独自に Decode 処理を実装し、contains(.KEY) メソッドでレスポンスに該当フィールドがあるか(値が何であっても)を判定します。存在する場合は OptionalValue にデコードします。OptionalValue Enum 内で実際に必要な値をデコードし、値があれば .value(T) に、null(またはデコード失敗)の場合は .null に格納します。

  1. Response にフィールドと値がある場合:OptionalValue.value(VALUE)

  2. Response にフィールドと値が null の場合:OptionalValue.null

  3. Response がフィールドを返さない場合:nil

これによりフィールドの存在有無を区別でき、後で Core Data に書き込む際にフィールドを null に更新するかどうか判断できます。

その他の検討 — Double Optional ❌

Optional!Optional! は Swift でこのシーンに非常に適しています。

1
2
3
4
5
6
7
8
9
struct Song: Decodable {
    var id: Int
    var name: String??
    var file: String??
    var converImage: String??
    var likeCount: Int??
    var like: Bool??
    var length: Int??
}
  1. Response にフィールドと値がある場合:Optional(VALUE)

  2. Response にフィールドがあり値が null の場合:Optional(nil)

  3. Response がフィールドを返さない場合:nil

しかし….Codable の JSONDecoder Decode は Double Optional と Optional の両方を decodeIfPresent で処理し、どちらも Optional と見なすため、Double Optional を特別に扱いません。そのため、結果は元のままです。

その他の検討事項 — Property Wrapper ❌

もともと Property Wrapper を使って優雅にラップできると考えていました。例えば:

1
@OptionalValue var file: String?

しかし、詳細を調べる前に、Property Wrapper が付いた Codable プロパティのフィールドがある場合、API レスポンスには必ずそのフィールドが存在しなければならず、たとえそのフィールドが Optional であっても、存在しないと keyNotFound エラーが発生します。?????

公式フォーラムにもこの問題に関するディスカッションスレッドがあり…将来的に修正される見込みです。

そのため、BetterCodableCodableWrappers のようなライブラリを選ぶ際には、現在の Property Wrapper のこの問題を考慮する必要があります。

その他の問題シナリオ

1.APIレスポンスで0/1がBoolを表す場合、どうデコードする?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Foundation

struct Song: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case like
    }
    
    var id: Int
    var name: String?
    var like: Bool?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.name = try container.decodeIfPresent(String.self, forKey: .name)
        
        if let intValue = try container.decodeIfPresent(Int.self, forKey: .like) {
            self.like = (intValue == 1) ? true : false // 1ならtrue、それ以外はfalse
        } else if let boolValue = try container.decodeIfPresent(Bool.self, forKey: .like) {
            self.like = boolValue
        }
    }
}

var jsonData = """
{
    "id": 1,
    "name": "告五人",
    "like": 0
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

前回の記事を踏まえて、init Decode 内で int や Bool にデコードし、自分で値を代入することで、元のフィールドが 0/1/true/false を受け入れられるように拡張できます。

2. 毎回 init decoder を書き直したくない場合

自分で Decoder を実装したくない場合、既存の JSON Decoder を拡張して機能を追加する。

私たちは独自に KeyedDecodingContainer を extension して public メソッドを定義できます。Swift はモジュール内で再定義したメソッドを優先して実行し、元の Foundation の実装を上書きします。

影響するのはモジュール全体です。

そして本当の override ではなく、super.decode を呼び出せないため、自分自身を呼び出さないよう注意が必要です(例:decode(Bool.Type, for: key) 内で decode(Bool.Type, for: key) を呼ぶなど)

decode には2つの方法があります:

  • decode(Type, forKey:) が Optional でないデータフィールドを処理する場合

  • decodeIfPresent(Type, forKey:) は Optional なデータフィールドを処理する方法

例1. 前述の主な問題を私たちは直接 extension で解決できます:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension KeyedDecodingContainer {
    public func decodeIfPresent<T>(_ type: T.Type, forKey key: Self.Key) throws -> T? where T : Decodable {
        //より良い方法:
        switch type {
        case is OptionalValue<String>.Type,
             is OptionalValue<Int>.Type:
            return try? decode(type, forKey: key)
        default:
            return nil
        }
        // または単に return try? decode(type, forKey: key)
    }
}

struct Song: Decodable {
    var id: Int
    var file: OptionalValue<String>?
}

主な問題は Optional データフィールドと Decodable 型であるため、私たちは decodeIfPresent<T: Decodable> メソッドをオーバーライドしています。

ここでは、元々の decodeIfPresent の実装は、データが null またはレスポンスに存在しない場合、実際に decode を実行せずに直接 nil を返すと推測しています。

なので原理はとても簡単で、Decodable 型が OptionValue であれば、どんな場合でもデコードしてみます。そうすることで異なる状態の結果を取得できます。ただし、Decodable 型を判定しなくても構いません。その場合はすべての Optional 項目をデコードしようとします。

例2. 問題シーン1にもこの方法で拡張可能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
extension KeyedDecodingContainer {
    public func decodeIfPresent(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool? {
        if let intValue = try? decodeIfPresent(Int.self, forKey: key) {
            return (intValue == 1) ? (true) : (false) // 1ならtrue、それ以外はfalseを返す
        } else if let boolValue = try? decodeIfPresent(Bool.self, forKey: key) {
            return boolValue // Bool値を返す
        }
        return nil // 値が存在しない場合はnilを返す
    }
}

struct Song: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case like
    }
    
    var id: Int
    var name: String?
    var like: Bool?
}

var jsonData = """
{
    "id": 1,
    "name": "告五人",
    "like": 1
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)

結語

Codable のさまざまなテクニックはほぼ使い尽くしましたが、いくつかはかなり複雑です。Codable の制約が非常に強いため、実際の開発で必要な柔軟性を多く犠牲にしています。最終的には、なぜ最初に Codable を選んだのかを考え始めるほどで、利点がどんどん減っていきます…。

参考資料

振り返り

ご質問やご意見がございましたら、こちらからご連絡ください

Post MediumからZMediumToMarkdownで変換。


🍺 Buy me a beer on PayPal

👉👉👉 Follow Me On Medium! (1,053+ Followers) 👈👈👈

本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

Improve this page on Github.

本記事は著者により CC BY 4.0 に基づき公開されています。

© ZhgChgLi. All rights reserved.
閲覧数: 802,415+, 最終更新日時: 2026-01-15 11:14:58 +08:00

本サイトは Chirpy テーマを使用し、Jekyll 上で構築されています。
Medium の記事は ZMediumToMarkdown により変換されています。