記事

Codable デコード問題の完全ガイド|基礎から応用まで全シーン対応

iOS開発者向けにCodableのデコード問題を網羅的に解説。デコードエラーの原因特定から解決策まで具体例で示し、効率的なデータ処理を実現します。

Codable デコード問題の完全ガイド|基礎から応用まで全シーン対応

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

記事一覧


実際に Codable を使う際に直面する Decode の問題シナリオまとめ(上)

基礎から応用まで、Decodableを深く活用してあらゆる問題シナリオに対応する

Photo by [Gustas Brazaitis](https://unsplash.com/@gustasbrazaitis){:target="_blank"}

Photo by Gustas Brazaitis

はじめに

後端 API のアップグレードに伴い、API 処理の構造を調整する必要がありました。この機会に、従来 Objective-C で書かれていたネットワーク処理の構造を Swift に更新しました。言語が異なるため、以前使用していた Restkit はネットワーク層の処理には適さなくなりました。しかし、Restkit の機能は非常に豊富で、プロジェクト内でも活用されており、大きな問題はありませんでした。ただし、非常に重く、ほとんどメンテナンスされておらず、純粋な Objective-C であるため、将来的には必ず置き換える必要があります。

Restkit はほぼすべてのネットワークリクエスト関連の機能を処理してくれます。基本的なネットワーク処理、API 呼び出し、ネットワーク処理から、レスポンスの JSON 文字列をオブジェクトに変換すること、さらにはオブジェクトを Core Data に保存することまで、一つのフレームワークでまとめて実現できる、非常に強力なフレームワークです。

時代の進化に伴い、現在のフレームワークはすべてを一括で提供するのではなく、より柔軟で軽量、組み合わせ可能な形を主流としています。これにより、より多くの柔軟性と多様な変化を生み出しています。そのため、Swift言語に移行する際には、ネットワーク処理部分には Moya を採用し、その他必要な機能は別の方法で組み合わせていくことを選択しました。

本題

JSON String to Object Mapping の部分については、Swift 標準の Codable(特に Decodable)プロトコルと JSONDecoder を使用して処理しています。また、Entity/Model を分割して責任範囲の明確化、操作性および可読性の向上を図っています。さらに、コードベースが Objective-C と Swift の混在である点も考慮しています。

* Encodable の部分は省略し、例はすべて Decodable の実装のみを示しています。ほぼ同様で、Decode ができれば基本的に Encode も可能です。

開始

初期の API レスポンスの JSON 文字列は以下の通りです:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "id": 123456,
  "comment": "是告五人,不是五告人!",
  "target_object": {
    "type": "song",
    "id": 99,
    "name": "披星戴月的想你"
  },
  "commenter": {
    "type": "user",
    "id": 1,
    "name": "zhgchgli",
    "email": "[email protected]"
  }
}

上記の例から、User/Song/Comment の3つのエンティティ&モデルに分割できます。これにより再利用が可能になります。便宜上、表示のためにエンティティ/モデルを同じファイルに記述します。

ユーザー:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// エンティティ:
struct UserEntity: Decodable {
    var id: Int
    var name: String
    var email: String
}

// モデル:
class UserModel: NSObject {
    init(_ entity: UserEntity) {
      self.id = entity.id
      self.name = entity.name
      self.email = entity.email
    }
    var id: Int
    var name: String
    var email: String
}

曲:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// エンティティ:
struct SongEntity: Decodable {
    var id: Int
    var name: String
}

// モデル:
class SongModel: NSObject {
    init(_ entity: SongEntity) {
      self.id = entity.id
      self.name = entity.name
    }
    var id: Int
    var name: String
}

コメント:

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
// エンティティ:
struct CommentEntity: Decodable {
    enum CodingKeys: String, CodingKey {
      case id
      case comment
      case targetObject = "target_object"
      case commenter
    }
    
    var id: Int
    var comment: String
    var targetObject: SongEntity
    var commenter: UserEntity
}

// モデル:
class CommentModel: NSObject {
    init(_ entity: CommentEntity) {
      self.id = entity.id
      self.comment = entity.comment
      self.targetObject = SongModel(entity.targetObject)
      self.commenter = UserModel(entity.commenter)
    }
    var id: Int
    var comment: String
    var targetObject: SongModel
    var commenter: UserModel
}

JSONDecoder:

1
2
3
4
5
6
7
let jsonString = "{ \"id\": 123456, \"comment\": \"是告五人,不是五告人!\", \"target_object\": { \"type\": \"song\", \"id\": 99, \"name\": \"披星戴月的想你\" }, \"commenter\": { \"type\": \"user\", \"id\": 1, \"name\": \"zhgchgli\", \"email\": \"[email protected]\" } }"
let jsonDecoder = JSONDecoder()
do {
    let result = try jsonDecoder.decode(CommentEntity.self, from: jsonString.data(using: .utf8)!)
} catch {
    print(error)
}

CodingKeys Enum?

JSONのキー名とEntityオブジェクトのプロパティ名が一致しない場合は、内部にCodingKeysという列挙型を追加して対応できます。バックエンドのデータソースの命名規則は私たちが制御できないためです。

1
2
case PropertyKeyName = "後端フィールド名"
case PropertyKeyName // 指定しない場合はデフォルトで PropertyKeyName を後端フィールド名として使用

CodingKeys 列挙体を追加すると、Optional でないすべてのフィールドを列挙する必要があり、カスタマイズしたいキーだけを列挙することはできません。

もう一つの方法は JSONDecoder の keyDecodingStrategy を設定することです。レスポンスのデータフィールドとプロパティ名が snake_casecamelCase の違いだけであれば、.keyDecodingStrategy.convertFromSnakeCase を設定するだけで自動的にマッピングが可能です。

1
2
3
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
try jsonDecoder.decode(CommentEntity.self, from: jsonString.data(using: .utf8)!)

返却データが配列の場合:

1
2
3
struct SongListEntity: Decodable {
    var songs:[SongEntity]
}

Stringに制約を追加する:

1
2
3
4
5
6
7
8
9
10
11
struct SongEntity: Decodable {
  var id: Int
  var name: String
  var type: SongType
  
  enum SongType {
    case rock
    case pop
    case country
  }
}

有限の範囲の文字列型に適用され、Enumとして定義すると渡しやすく使いやすいです;ただし、列挙されていない値が出現するとDecodeに失敗します!

ジェネリクスを活用して固定構造をラップする:

複数件の返却される JSON 文字列の固定フォーマットは以下の通りです:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "count": 10,
  "offset": 0,
  "limit": 0,
  "results": [
    {
      "type": "song",
      "id": 1,
      "name": "1"
    }
  ]
}

ジェネリックを使ってラップすることもできます:

1
2
3
4
5
6
struct PageEntity<E: Decodable>: Decodable {
    var count: Int
    var offset: Int
    var limit: Int
    var results: [E]
}

使用: PageEntity<Song>.self

Date/Timestamp 自動 Decode:

JSONDecoderdateDecodingStrategy の設定

  • .secondsSince1970/.millisecondsSince1970 : Unixタイムスタンプ

  • .deferredToDate : Appleのタイムスタンプで、あまり使われません。Unixタイムスタンプとは異なり、2001年1月1日からの経過時間を表します。

  • .iso8601 : ISO 8601 日付フォーマット

  • .formatted(DateFormatter) : 渡された DateFormatter に従って Date をデコードする

  • .custom : カスタム日付デコードロジック

.custom 例:APIが YYYY/MM/DD と ISO 8601 の2種類のフォーマットを返し、どちらもデコードできる場合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var dateFormatter = DateFormatter()
var iso8601DateFormatter = ISO8601DateFormatter()

let decoder: JSONDecoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
    let container = try decoder.singleValueContainer()
    let dateString = try container.decode(String.self)
    
    // ISO8601形式:
    if let date = iso8601DateFormatter.date(from: dateString) {
        return date
    }
    
    // YYYY-MM-DD形式:
    dateFormatter.dateFormat = "yyyy-MM-dd"
    if let date = dateFormatter.date(from: dateString) {
        return date
    }
    
    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
})

let result = try jsonDecoder.decode(CommentEntity.self, from: jsonString.data(using: .utf8)!)

*DateFormatter は初期化時に非常にパフォーマンスを消費するため、できるだけ再利用してください。

基本的な Decode の知識:

  1. Decodable プロトコル内のフィールドの型(struct/class/enum)は、すべて Decodable プロトコルを実装する必要があります。もしくは、init decoder 内で値を割り当てる必要があります。

  2. フィールドの型が一致しない場合、Decodeに失敗します

  3. Decodable オブジェクトのフィールドを Optional にすると、存在してもしなくてもよく、あればデコードされます。

  4. Optional フィールドは以下を許容します:JSON にフィールドがない場合、または存在していても nil が与えられている場合

  5. 空白や0はnilではありません。nilはnilです。弱い型付けのバックエンドAPIには注意が必要です!

  6. デフォルトの Decodable オブジェクトで列挙型かつ Optional でないフィールドがあり、JSON文字列にその値がなければデコードに失敗します(後述の対処方法を説明します)

  7. デフォルトでは、Decodeに失敗すると即座に処理が中断され、単純に誤ったデータをスキップすることはできません(後述で対処法を説明します)。

[左:”” 右:nil](https://josjong.com/2017/10/16/null-vs-empty-strings-why-oracle-was-right-and-apple-is-not/){:target="_blank"}

左:” “ / 右:nil

上級使用法

ここまでで基本的な使い方は完了しましたが、現実の世界はそれほど単純ではありません。以下にいくつかの応用的なシナリオを挙げ、Codable を使った解決策を提案します。ここからは、元の Decode による自動マッピングに頼れず、自分で init(from decoder: Decoder) を実装してカスタムデコード処理を行う必要があります。

*ここではひとまず Entity の部分だけを表示し、Model はまだ使用しません。

init(from decoder: Decoder)

init decoderでは、すべてのOptionalでないフィールドに初期値を与える必要があります(つまりinitです!)。

カスタムデコード操作を行う際、decoder から container を取得して値を操作する必要があります。container には3種類の取得方法があります。

第一種 container(keyedBy: CodingKeys.self) CodingKeys に従って操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct SongEntity: Decodable {
    var id: Int
    var name: String
    
    enum CodingKeys: String, CodingKey {
      case id
      case name
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        // パラメータ1は対応:Decodableを実装したクラス
        // パラメータ2はCodingKeys
        
        self.name = try container.decode(String.self, forKey: .name)
    }
}

第二の singleValueContainer で全体を取り出して操作する(単一値):

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
enum HandsomeLevel: Decodable {
    case handsome(String)
    case normal(String)
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let name = try container.decode(String.self)
        if name == "zhgchgli" {
            self = .handsome(name)
        } else {
            self = .normal(name)
        }
    }
}

struct UserEntity: Decodable {
    var id: Int
    var name: HandsomeLevel
    var email: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case email
    }
}

Associated Value Enum フィールドタイプに適用されます。例えば、name にかっこよさの度合いも含まれています!

第三の unkeyedContainer 全体を一つの配列として扱う:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ListEntity: Decodable {
    var items:[Decodable]
    init(from decoder: Decoder) throws {
        var unkeyedContainer = try decoder.unkeyedContainer()
        self.items = []
        while !unkeyedContainer.isAtEnd {
            // unkeyedContainer の内部ポインタは decode 操作後に自動で次の要素を指す
            // 終端に達するとループ終了を意味する
            if let id = try? unkeyedContainer.decode(Int.self) {
                items.append(id)
            } else if let name = try? unkeyedContainer.decode(String.self) {
                items.append(name)
            }
        }
    }
}

let jsonString = "[\"test\",1234,5566]"
let jsonDecoder = JSONDecoder()
let result = try jsonDecoder.decode(ListEntity.self, from: jsonString.data(using: .utf8)!)
print(result)

可変型の配列フィールドに適用。

Container の下で nestedContainer / nestedUnkeyedContainer を使って特定のフィールドを操作できます:

*データフィールドのフラット化(flatMapに類似)

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
struct ListEntity: Decodable {
    
    enum CodingKeys: String, CodingKey {
        case items
        case date
        case name
        case target
    }
    
    enum PredictKey: String, CodingKey {
        case type
    }
    
    var date: Date
    var name: String
    var items: [Decodable]
    var target: Decodable
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        self.date = try container.decode(Date.self, forKey: .date) // 日付をデコード
        self.name = try container.decode(String.self, forKey: .name) // 名前をデコード
        
        let nestedContainer = try container.nestedContainer(keyedBy: PredictKey.self, forKey: .target)
        
        let type = try nestedContainer.decode(String.self, forKey: .type) // タイプをデコード
        if type == "song" {
            self.target = try container.decode(SongEntity.self, forKey: .target) // SongEntityとしてデコード
        } else {
            self.target = try container.decode(UserEntity.self, forKey: .target) // UserEntityとしてデコード
        }
        
        var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .items)
        self.items = []
        while !unkeyedContainer.isAtEnd {
            if let song = try? unkeyedContainer.decode(SongEntity.self) {
                items.append(song) // SongEntityを追加
            } else if let user = try? unkeyedContainer.decode(UserEntity.self) {
                items.append(user) // UserEntityを追加
            }
        }
    }
}

異なる階層のオブジェクトへのアクセスとデコードの例として、target/items に対して nestedContainer と flat を使い、type を取得してから type に応じたデコードを行う方法を示します。

Decode & DecodeIfPresent

  • DecodeIfPresent: レスポンスにデータフィールドが存在する場合のみデコードを行う(CodableのプロパティがOptionalの場合)

  • Decode: デコードを実行し、レスポンスにデータフィールドがない場合はエラーをスローします

*以上は init decoder や container のメソッドや機能を簡単に紹介しただけです。理解できなくても問題ありません。さっそく実際のシーンに入り、サンプルで組み合わせた操作方法を体験しましょう。

現実のシナリオ

元のサンプル JSON 文字列に戻ります。

シナリオ1. 今日、誰かにコメントするとき、それが曲へのコメントか人へのコメントか分からない場合、targetObject フィールドの対象は User または Song のどちらでしょうか?どう処理すればよいですか?

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
{
  "results": [
    {
      "id": 123456,
      "comment": "是告五人,不是五告人!",
      "target_object": {
        "type": "song",
        "id": 99,
        "name": "披星戴月的想你"
      },
      "commenter": {
        "type": "user",
        "id": 1,
        "name": "zhgchgli",
        "email": "[email protected]"
      }
    },
    {
      "id": 55,
      "comment": "66666!",
      "target_object": {
        "type": "user",
        "id": 1,
        "name": "zhgchgli"
      },
      "commenter": {
        "type": "user",
        "id": 2,
        "name": "aaaa",
        "email": "[email protected]"
      }
    }
  ]
}

方法 a.

Enum をコンテナとして使用したデコード。

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
struct CommentEntity: Decodable {
    
    enum CodingKeys: String, CodingKey {
      case id
      case comment
      case targetObject = "target_object"
      case commenter
    }
    
    var id: Int
    var comment: String
    var targetObject: TargetObject
    var commenter: UserEntity
    
    enum TargetObject: Decodable {
        case song(SongEntity)
        case user(UserEntity)
        
        enum PredictKey: String, CodingKey {
            case type
        }
        
        enum TargetObjectType: String, Decodable {
            case song
            case user
        }
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: PredictKey.self)
            let singleValueContainer = try decoder.singleValueContainer()
            let targetObjectType = try container.decode(TargetObjectType.self, forKey: .type)
            
            switch targetObjectType {
            case .song:
                let song = try singleValueContainer.decode(SongEntity.self)
                self = .song(song)
            case .user:
                let user = try singleValueContainer.decode(UserEntity.self)
                self = .user(user)
            }
        }
    }
}

targetObject のプロパティを Associated Value Enum に変更し、Decode 時に Enum の中に何を入れるかを決定します。

核心の実践は、Decodableに準拠したEnumをコンテナとして作成し、decode時にまずキーとなるフィールド(例としてJSON文字列中のtypeフィールド)を取得して判別します。Songの場合はsingleValueContainerを使って全体をSongEntityにデコードし、Userの場合も同様に処理します。

使用時に Enum から取り出す:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//if case let
if case let CommentEntity.TargetObject.user(user) = result.targetObject {
    print(user)
} else if case let CommentEntity.TargetObject.song(song) = result.targetObject {
    print(song)
}

//switch case let
switch result.targetObject {
case .song(let song):
    print(song)
case .user(let user):
    print(user)
}

方法 b.

宣言フィールドの属性をベースクラスに変更する。

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
struct CommentEntity: Decodable {
    enum CodingKeys: String, CodingKey {
      case id
      case comment
      case targetObject = "target_object"
      case commenter
    }
    enum PredictKey: String, CodingKey {
        case type
    }
    
    var id: Int
    var comment: String
    var targetObject: Decodable
    var commenter: UserEntity
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.comment = try container.decode(String.self, forKey: .comment)
        self.commenter = try container.decode(UserEntity.self, forKey: .commenter)
        
        //
        let targetObjectContainer = try container.nestedContainer(keyedBy: PredictKey.self, forKey: .targetObject)
        let targetObjectType = try targetObjectContainer.decode(String.self, forKey: .type)
        if targetObjectType == "user" {
            self.targetObject = try container.decode(UserEntity.self, forKey: .targetObject)
        } else {
            self.targetObject = try container.decode(SongEntity.self, forKey: .targetObject)
        }
    }
}

原理はほぼ同じですが、ここではまず nestedContainer を使って targetObject に入り、type を取り出して判定し、その後 targetObject をどのタイプに解析するか決定します。

使用時にキャストする:

1
2
3
4
5
if let song = result.targetObject as? Song {
  print(song)
} else if let user = result.targetObject as? User {
  print(user)
}

シナリオ2. 配列フィールドに複数の種類のデータがある場合、どのようにDecodeするか?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "results": [
    {
      "type": "song",
      "id": 99,
      "name": "披星戴月的想你"
    },
    {
      "type": "user",
      "id": 1,
      "name": "zhgchgli",
      "email": "[email protected]"
    }
  ]
}

上述で述べた nestedUnkeyedContainer と シナリオ1の解決策を組み合わせることができます;ここではシナリオ1の a. 解決策 を使い、Associated Value Enum で値を取得する方法にも変更可能です。

シナリオ3. JSONのStringフィールドに値がある場合のみDecodeする

1
2
3
4
5
6
7
8
9
10
11
[
  {
    "type": "song",
    "id": 99,
    "name": "披星戴月的想你"
  },
    {
    "type": "song",
    "id": 11
  }
]

decodeIfPresent を使ったデコード。

シナリオ4. 配列データでDecode失敗したデータをスキップする

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "results": [
    {
      "type": "song",
      "id": 99,
      "name": "披星戴月的想你"
    },
    {
      "error": "errro"
    },
    {
      "type": "song",
      "id": 19,
      "name": "帶我去找夜生活"
    }
  ]
}

前述のように、Decodable はデフォルトで全てのデータ解析が正しく行われて初めてマッピング出力されます。時にはバックエンドからのデータが不安定で、長い配列が渡されても一部のデータでフィールドが欠けていたり、フィールドの型が合わずにデコードに失敗することがあります。その結果、全体が失敗して nil になってしまいます。

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
struct ResultsEntity: Decodable {
    enum CodingKeys: String, CodingKey {
        case results
    }
    var results: [SongEntity]
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        var nestedUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .results)
        
        self.results = []
        while !nestedUnkeyedContainer.isAtEnd {
            if let song = try? nestedUnkeyedContainer.decode(SongEntity.self) {
                self.results.append(song)
            } else {
                let _ = try nestedUnkeyedContainer.decode(EmptyEntity.self) // 空のエンティティをデコードしてスキップ
            }
        }
    }
}

struct EmptyEntity: Decodable { }

struct SongEntity: Decodable {
    var type: String
    var id: Int
    var name: String
}

let jsonString = "{ \"results\": [ { \"type\": \"song\", \"id\": 99, \"name\": \"披星戴月的想你\" }, { \"error\": \"errro\" }, { \"type\": \"song\", \"id\": 19, \"name\": \"帶我去找夜生活\" } ] }"
let jsonDecoder = JSONDecoder()
let result = try jsonDecoder.decode(ResultsEntity.self, from: jsonString.data(using: .utf8)!)
print(result)

解決方法も 場面2.の解決策 と似ています;nestedUnkeyedContainer で各要素を繰り返し処理し、try? Decode を実行します。Decode に失敗した場合は Empty Decode を使い、nestedUnkeyedContainer の内部ポインタを進め続けます。

*この方法は少しワークアラウンドです。nestedUnkeyedContainer に対してスキップ命令ができず、nestedUnkeyedContainer はデコードに成功しないと次に進まないため、このようにしています。Swiftコミュニティでは moveNext( ) の追加が提案されていますが、現バージョンではまだ実装されていません。

シナリオ5. 一部のフィールドはプログラム内部で使用し、Decodeする必要がない場合

方法a. Entity/Model

ここで最初に述べた、Entity/Model を分割する役割について触れます。Entity は純粋に JSON 文字列から Entity(Decodable)へのマッピングを担当し、Model は Entity を初期化し、実際のプログラムの受け渡し、操作、ビジネスロジックはすべて Model を使って行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct SongEntity: Decodable {
    var type: String
    var id: Int
    var name: String
}

class SongModel: NSObject {
    init(_ entity: SongEntity) {
        self.type = entity.type
        self.id = entity.id
        self.name = entity.name
    }
    
    var type: String
    var id: Int
    var name: String
    
    var isSave:Bool = false // ビジネスロジック
}

Entity/Model を分割するメリット:

  1. 責任を明確に分ける、Entity: JSON文字列からDecodableへ、Model: ビジネスロジック

  2. 一目でどのフィールドがマッピングされているか Entity を見ればわかる

  3. フィールドが多くなるとすべてが一緒に詰まってしまうのを避ける

  4. Objective-C でも使用可能 (Model は NSObject や struct/Decodable であり、Objective-C からは見えません)

  5. 内部で使用するビジネスロジックやフィールドは Model に置くだけでよい

方法b. initでの処理

CodingKeys を列挙し、内部使用のフィールドを除外します。init 時にデフォルト値を与えたり、フィールドにデフォルト値を設定したり、Optional にする方法もありますが、どれも良い方法ではなく、単に動作させるためだけのものです。

[2020/06/26 更新] — 後編 シナリオ6.APIレスポンスで Bool を 0/1 で表現している場合の Decode 方法は?

[2020/06/26 更新] — 後編 シーン7. 毎回 init decoder を書き直したくない場合

[2020/06/26 更新] — 後編 シナリオ8. Responseの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
48
{
  "count": 5,
  "offset": 0,
  "limit": 10,
  "results": [
    {
      "id": 123456,
      "comment": "是告五人,不是五告人!",
      "target_object": {
        "type": "song",
        "id": 99,
        "name": "披星戴月的想你",
        "create_date": "2020-06-13T15:21:42+0800"
      },
      "commenter": {
        "type": "user",
        "id": 1,
        "name": "zhgchgli",
        "email": "[email protected]",
        "birthday": "1994/07/18"
      }
    },
    {
      "error": "not found"
    },
    {
      "error": "not found"
    },
    {
      "id": 2,
      "comment": "哈哈,我也是!",
      "target_object": {
        "type": "user",
        "id": 1,
        "name": "zhgchgli",
        "email": "[email protected]",
        "birthday": "1994/07/18"
      },
      "commenter": {
        "type": "user",
        "id": 1,
        "name": "路人甲",
        "email": "[email protected]",
        "birthday": "2000/01/12"
      }
    }
  ]
}

Output:

1
zhgchgli: 告五人です、「五告人」ではありません!

完全なサンプルは上記の通りです!

(下)編&その他のシーンを更新しました:

まとめ

Codableを選ぶ利点は、まず第一にネイティブであるため、後のメンテナンスが心配なく、コードも美しく書けることです。しかし、その反面制約が厳しく、JSON文字列の柔軟な解析は難しいです。そうした場合は本文のように多くの工夫が必要になります。また、性能面では他のマッピングライブラリに劣ることもあります(Decodableは依然としてObjective-C時代のNSJSONSerializationを使って解析しています)。ですが、今後のアップデートでAppleがこれを改善する可能性があり、その際はコードを変更する必要がなくなるでしょう。

本文のシナリオや例はやや極端な場合もありますが、時にはそういった状況に直面することもあります。もちろん、通常はシンプルな Codable で十分に対応できることを望みます。しかし、上記のテクニックを身につければ、ほとんどの問題は解決できるはずです!

感謝 @saiday さんの技術サポート。

関連記事

  1. 深入 Decodable — — 原生を超える JSON パーサーを書く 内容が充実しており、Decoder/JSONDecoder を深く理解できます。

  2. 異なる視点から問題を見る — Codable から Swift メタプログラミングへ

  3. なぜモデルオブジェクトは Swift の Decodable や Encodable プロトコルを実装すべきでないのか

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

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 に基づき公開されています。