実際に Codable を使う際に遭遇する Decode の問題シーンまとめ(上)
基礎から応用まで、Decodable を使ってあらゆる問題シーンに対応する方法を徹底解説

Photo by Gustas Brazaitis
はじめに
後端のAPIアップグレードに伴い、API処理のアーキテクチャを調整する必要がありました。この機会に、元々Objective-Cで書かれていたネットワーク処理のアーキテクチャをSwiftに更新しました。言語が異なるため、従来のRestkitを使ってネットワーク層を処理するのは適切ではなくなりました。しかし、Restkitは機能が非常に豊富で、プロジェクト内でうまく活用できており、大きな問題はほとんどありませんでした。一方で、非常に重く、ほとんどメンテナンスされておらず、純粋なObjective-Cで書かれているため、将来的には必ず置き換える必要があります。
Restkit は、基本的なネットワーク処理、API 呼び出し、ネットワーク処理からレスポンスの JSON 文字列をオブジェクトに変換、さらにはオブジェクトを Core Data に保存するまで、ネットワーク関連のほぼすべての機能をほぼ完璧に処理してくれる、実質的に一つのフレームワークで十人分の働きをする存在です。
時代の進化に伴い、現在のフレームワークはすべてを一つにまとめるのではなく、より柔軟で軽量、そして組み合わせ可能なものが主流となっています。これにより、より多くの柔軟性と多様な変化を生み出しています。そのため、Swift言語に移行する際には、ネットワーク処理部分にMoyaを採用し、その他必要な機能は別の方法で組み合わせて選択しています。
本題
JSON文字列からオブジェクトへのマッピングについては、Swift標準の Codable(主に Decodable)プロトコルと JSONDecoder を使用して処理しています。また、Entity と Model を分離して責任範囲を明確にし、操作性と可読性を向上させています。さらに、コードベースが Objective-C と Swift の混在である点も考慮しています。
* Encodable の部分は省略し、例はすべて Decodable の実装のみを示しています。ほぼ同様で、Decode ができれば基本的に Encode も可能です。
開始
初期の API レスポンス JSON 文字列は以下の通りとします:
{
"id": 123456,
"comment": "是告五人,不是五告人!",
"target_object": {
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
"commenter": {
"type": "user",
"id": 1,
"name": "zhgchgli",
"email": "[email protected]"
}
}
上記の例から、User/Song/Comment の3つの Entity と Model に分割できます。これにより再利用が可能になります。便宜上、表示のために Entity と Model を同じファイルに記述します。
User:
// エンティティ:
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
}
曲:
// エンティティ:
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
}
コメント:
// エンティティ:
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:
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という列挙型を追加して対応できます。バックエンドのデータソースの命名規則は私たちがコントロールできないためです。
case PropertyKeyName = "バックエンドのフィールド名"
case PropertyKeyName // 指定しない場合はデフォルトで PropertyKeyName をバックエンドのフィールド名として使用
CodingKeys 列挙型を追加すると、Optional でないすべてのフィールドを列挙する必要があり、カスタマイズしたいキーだけを列挙することはできません。
もう一つの方法は JSONDecoder の keyDecodingStrategy を設定することです。レスポンスのデータフィールドとプロパティ名が snake_case と camelCase の違いだけであれば、.keyDecodingStrategy を .convertFromSnakeCase に設定することで自動的にマッピングが可能です。
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
try jsonDecoder.decode(CommentEntity.self, from: jsonString.data(using: .utf8)!)
返却データが配列の場合:
struct SongListEntity: Decodable {
var songs:[SongEntity]
}
String に制約を付ける:
struct SongEntity: Decodable {
var id: Int
var name: String
var type: SongType
enum SongType {
case rock
case pop
case country
}
}
限定された範囲の文字列型に適用され、Enumとして書くことで渡したり使用したりしやすくなります;列挙にない値が出現するとDecodeに失敗します!
ジェネリクスを活用して固定構造をラップする:
複数件返却される JSON 文字列の固定フォーマットは以下の通りです:
{
"count": 10,
"offset": 0,
"limit": 0,
"results": [
{
"type": "song",
"id": 1,
"name": "1"
}
]
}
ジェネリック型でラップすることができます:
struct PageEntity<E: Decodable>: Decodable {
var count: Int
var offset: Int
var limit: Int
var results: [E]
}
使用: PageEntity<Song>.self
Date/Timestamp 自動デコード:
JSONDecoder の dateDecodingStrategy の設定方法
-
.secondsSince1970/.millisecondsSince1970: Unixタイムスタンプ -
.deferredToDate: Appleのタイムスタンプで、あまり使われません。Unixタイムスタンプとは異なり、2001年1月1日からの経過時間を表します。 -
.iso8601: ISO 8601 日付形式 -
.formatted(DateFormatter): 渡された DateFormatter に従って日付をデコードする -
.custom: カスタムの Date デコードロジック
.custom 例:APIが YYYY/MM/DD と ISO 8601 の2種類の形式を返す場合、両方をデコードできるようにする:
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の知識:
-
Decodable プロトコル内のフィールドの型(struct/class/enum)はすべて Decodable プロトコルを実装する必要があります。または、init(decoder) 内で値を割り当てる必要があります。
-
フィールドの型が一致しない場合、Decodeに失敗します。
-
Decodable オブジェクトのフィールドを Optional にすると、存在すればデコードし、なくても問題ありません。
-
Optional なフィールドが許容される場合:JSON の文字列にフィールドがない、またはフィールドが存在しても nil が与えられている場合。
-
空白や0はnilではありません。nilはnilです。型の緩いバックエンドAPIには注意が必要です!
-
デフォルトの Decodable オブジェクトで、列挙型かつ Optional でないフィールドがある場合、JSON 文字列に該当フィールドがないとデコードに失敗します(後述の対処法があります)。
-
デフォルトでは、Decode に失敗するとその場で中断してしまい、エラーのあるデータを単純にスキップすることはできません(後ほど対処方法を説明します)。

高度な使用法
ここまでで基本的な使い方は完了しましたが、現実の世界はそんなに簡単ではありません。以下にいくつかの応用的なシナリオを挙げ、Codable を使った解決策を提案します。ここからは元の Decode によるマッピング補完はできないため、自分で init(from decoder: Decoder) を実装してカスタムデコード操作を行う必要があります。
*ここではひとまず Entity の部分のみを表示し、Model はまだ使用しません。
init(from decoder: Decoder)
init decoderでは、すべてのOptionalでないフィールドに初期値を与える必要があります(つまりinitです!)。
自分で Decode 操作を行う際、decoder から container を取得して値を取り出す必要があります。container には3種類の内容取得タイプがあります。

第一種 container(keyedBy: CodingKeys.self) CodingKeys に従って操作:
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 で全体を一括取得(単一値):
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 全体を一つの配列として扱う:
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に類似)

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 を使って type を取得し、その type に応じて対応するデコードを行う方法を示しています。
Decode と DecodeIfPresent
-
DecodeIfPresent: Response にデータフィールドが存在する場合のみデコードを行う(Codable プロパティが Optional の場合)
-
Decode: デコード操作を行う。レスポンスにデータフィールドがない場合はエラーをスローする
*以上は init decoder や container の方法や機能を簡単に紹介しただけです。わからなくても問題ありません。実際のシーンに進み、サンプルで組み合わせた操作方法を体感しましょう。
現実のシナリオ
元のサンプル JSON 文字列に戻ります。
シナリオ1. 今日、誰かにコメントを残す場合、それが曲に対するものか人に対するものか分からないとします。targetObject フィールドの対象は User または Song のどちらでしょうか?どう対応すればよいですか?
{
"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 をコンテナとして使ったデコード。
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 String の type フィールド)を取得して判別します。type が Song の場合は singleValueContainer を使って全体を SongEntity にデコードし、User の場合も同様に処理します。
使用時に Enum から取り出す:
//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.
宣言フィールドのプロパティをベースクラスに変更する。
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 をどの型に解析するか決定します。
使用時にキャストする:
if let song = result.targetObject as? Song {
print(song)
} else if let user = result.targetObject as? User {
print(user)
}
シナリオ2. 配列フィールドに複数の種類のデータが入っている場合、どのようにDecodeするか?
{
"results": [
{
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
{
"type": "user",
"id": 1,
"name": "zhgchgli",
"email": "[email protected]"
}
]
}
struct ListEntity: Decodable {
enum CodingKeys: String, CodingKey {
case results
}
enum PredictKey: String, CodingKey {
case type
}
var results:[Decodable]
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 {
// type を取得して判別
let type = try nestedUnkeyedContainer.nestedContainer(keyedBy: PredictKey.self).decode(String.self, forKey: .type)
if type == "song" {
results.append(try nestedUnkeyedContainer.decode(SongEntity.self))
} else {
results.append(try nestedUnkeyedContainer.decode(UserEntity.self))
}
}
}
}
上述で述べた nestedUnkeyedContainer と場面1の解決策を組み合わせることができます;ここでも場面1の a. 解決策 を使い、Associated Value Enum で値を取得する方法に変更可能です。
シナリオ3. JSONのStringフィールドに値がある場合のみDecodeする
[
{
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
{
"type": "song",
"id": 11
}
]
struct TargetEntity: Decodable {
enum CodingKeys: String, CodingKey {
case type
case id
case name
}
var type: String
var id: Int
var name: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.type = try container.decode(String.self, forKey: .type)
// 方法1:
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
// または方法2:
self.name = (try? container.decode(String.self, forKey: .name)) ?? "" // 良くない
}
}
let jsonString = "[ { \"type\": \"song\", \"id\": 99, \"name\": \"披星戴月的想你\" }, { \"type\": \"song\", \"id\": 11 } ]"
let jsonDecoder = JSONDecoder()
let result = try jsonDecoder.decode([TargetEntity].self, from: jsonString.data(using: .utf8)!)
decodeIfPresent を使ったデコード。
シナリオ4. 配列データでDecode失敗したデータをスキップする
{
"results": [
{
"type": "song",
"id": 99,
"name": "星をかぶり月をかぶったあなたを想う"
},
{
"error": "errro"
},
{
"type": "song",
"id": 19,
"name": "ナイトライフを探しに連れてって"
}
]
}
前述の通り、Decodable はデフォルトで全てのデータ解析が正しく行われた場合にのみマッピングして出力します。時にはバックエンドからのデータが不安定で、長い配列の中にいくつかの項目でフィールドが欠けていたり、フィールドの型が合わずに Decode に失敗することがあります。そのため、全体が失敗して nil になってしまいます。
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を初期化して、実際のプログラム内でのデータ受け渡しや操作、ビジネスロジックを担います。
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 を分けるメリット:
-
責任を明確に分ける、Entity: JSON文字列からDecodableへ、Model: ビジネスロジック
-
一目でどのフィールドがマッピングされているか Entity を見ればわかる
-
フィールドが多くて全部一緒にまとめないようにする
-
Objective-C でも使用可能(Model は単なる NSObject、struct/Decodable は Objective-C から見えないため)
-
内部で使用するビジネスロジックやフィールドは 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フィールドデータの適切な処理
総合シナリオ例
以上の基本的な使用法と応用例を総合した完全なサンプル:
{
"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"
}
}
]
}
import Foundation
//
let jsonString = """
{
"count": 3,
"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"
}
}
]
}
"""
//
// Entity:
struct SongEntity: Decodable {
enum CodingKeys: String, CodingKey {
case type
case id
case name
case createDate = "create_date"
}
var type: String
var id: Int
var name: String
var createDate: Date
}
struct UserEntity: Decodable {
var type: String
var id: Int
var name: String
var email: String
var birthday: Date
}
struct CommentEntity: Decodable {
enum CodingKeys: String, CodingKey {
case id
case comment
case commenter
case targetObject = "target_object"
}
enum PredictKey: String, CodingKey {
case type
}
enum ObjectType: String, Decodable {
case song
case user
}
var id: Int
var comment: String
var commenter: UserEntity
var targetObject: Decodable
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)
// targetObject は UserEntity または SongEntity になる可能性があります
let targetObjectNestedContainer = try container.nestedContainer(keyedBy: PredictKey.self, forKey: .targetObject)
let type = try targetObjectNestedContainer.decode(ObjectType.self, forKey: .type)
switch type {
case .song:
self.targetObject = try container.decode(SongEntity.self, forKey: .targetObject)
case .user:
self.targetObject = try container.decode(UserEntity.self, forKey: .targetObject)
}
}
}
struct EmptyEntity: Decodable { }
struct PageEntity<E: Decodable>: Decodable {
enum CodingKeys: String, CodingKey {
case count
case offset
case limit
case results
}
var count: Int
var offset: Int
var limit: Int
var results: [E]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.count = try container.decode(Int.self, forKey: .count)
self.offset = try container.decode(Int.self, forKey: .offset)
self.limit = try container.decode(Int.self, forKey: .limit)
var nestedUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .results)
self.results = []
while !nestedUnkeyedContainer.isAtEnd {
if let entity = try? nestedUnkeyedContainer.decode(E.self) {
self.results.append(entity)
} else {
let _ = try nestedUnkeyedContainer.decode(EmptyEntity.self)
}
}
}
}
// Model:
class UserModel: NSObject {
var type: String
var id: Int
var name: String
var email: String
var birthday: Date
init(_ entity: UserEntity) {
self.type = entity.type
self.id = entity.id
self.name = entity.name
self.email = entity.email
self.birthday = entity.birthday
}
}
class SongModel: NSObject {
var type: String
var id: Int
var name: String
var createDate: Date
init(_ entity: SongEntity) {
self.type = entity.type
self.id = entity.id
self.name = entity.name
self.createDate = entity.createDate
}
}
class CommentModel: NSObject {
var id: Int
var comment: String
var commenter: UserModel
var targetObject: NSObject?
var displayMessage: String // ビジネスロジックのシミュレーション
init(_ entity: CommentEntity) {
self.id = entity.id
self.comment = entity.comment
self.commenter = UserModel(entity.commenter)
if let userEntity = entity.targetObject as? UserEntity {
self.targetObject = UserModel(userEntity)
} else if let songEntity = entity.targetObject as? SongEntity {
self.targetObject = SongModel(songEntity)
}
self.displayMessage = "\(entity.commenter.name):\(entity.comment)"
}
}
//
let jsonDecoder = JSONDecoder()
let iso8601DateFormatter = ISO8601DateFormatter()
var dateFormatter = DateFormatter()
jsonDecoder.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: "日付文字列 \(dateString) をデコードできません")
})
do {
let pageEntity = try jsonDecoder.decode(PageEntity<CommentEntity>.self, from: jsonString.data(using: .utf8)!)
let comments = pageEntity.results.compactMap { CommentModel($0) }
comments.forEach { (comment) in
print(comment.displayMessage)
}
} catch {
print(error)
}
出力:
zhgchgli:告五人です、五告人ではありません!
路人甲:はは、私もです!
完全なサンプルは上記の通りです!
(下)編&その他のシナリオを更新しました:
まとめ
Codable を選ぶ利点は、まず第一にネイティブであるため、将来的にメンテナンスされなくなる心配がなく、コードも美しく書けることです。しかし、その分制約が厳しく、JSON文字列を柔軟に解釈するのは難しいです。そうでなければ、本記事のようにより多くの処理を実装する必要があります。また、パフォーマンスも他のマッピングライブラリに劣る場合があります(Decodable は依然として Objective-C 時代の NSJSONSerialization を使って解析しています)。ただし、今後のアップデートで Apple がこれを最適化する可能性があり、その際はコードを変更する必要がなくなるでしょう。
本文のシナリオや例は極端な場合もありますが、時にはそういった状況に遭遇することもあります。もちろん、通常はシンプルな Codable で十分なことを願っています。しかし、上記のテクニックを身につければ、ほとんどの問題は解決できるはずです!
感謝 @saiday さんの技術サポート。
延伸閱讀
- 深入 Decodable — — 写一个超越原生的 JSON 解析器 滿滿的內容,深入了解 Decoder/JSONDecoder。
- 不同角度看问题 — 从 Codable 到 Swift 元编程
- Why Model Objects Shouldn’t Implement Swift’s Decodable or Encodable Protocols
Post は Medium から ZMediumToMarkdown によって変換されました。



コメント