Codableのデコード時にオブジェクトの型を判定してデコードする

## やりたいこと

 - Codableを使って、微妙にkey:valueの異なるjsonオブジェクトをデコードしたい
 - 任意のkeyのvalueによってデコードする型を判別し、デコードしたい

## 具体的には?

例えば以下のようなjsonがあったとして、

 - `properties`配下のkey:valueが統一されていない
 - `properties`配下を`type`によって型を判定してデコードしたい

```json:articles.json
[
  {
    "id": "xxxxx",
    "name": "today's News",
    "properties": {
      "type": "News",
      "id": "xxxxxx",
      "name": "news title here",
      "backgroundColor": {
        "alpha": 0,
        "red": 0,
        "green": 0,
        "blue": 0
      }
    }
  },
  {
    "id": "xxxxx",
    "name": "weekly Gossip",
    "properties": {
      "type": "Gossip",
      "id": "xxxxxx",
      "name": "gossip title here",
      "radius": 3.6
    }
  },
  {
    "id": "xxxxx",
    "name": "Technology topic",
    "properties": {
      "type": "Tech",
      "id": "xxxxxx",
      "name": "tech title here",
      "refUrl": "https://xxx"
    }
  }
]
```

## どうパースしたらいいだろう？

結果から書くとこんな感じにしてみた。

```swift:dynamicClassDecode.playground
import Foundation
import UIKit

typealias Articles = [Article]

struct Article: Codable {
    let id, name: String
    let properties: Properties
}

extension Article {

    private enum CodingKeys: CodingKey {
        case id, name, properties
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(String.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        // decode temporarily to switch type and decode to each detailed classes
        let properties = try BaseProps.init(from: container.superDecoder(forKey: .properties))
        self.properties = try properties.type.metatype.init(from: container.superDecoder(forKey: .properties))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try properties.encode(to: container.superEncoder(forKey: .properties))
    }
}

protocol Properties: Codable {
    var type: PropertyType { get }
    var id: String { get }
    var name: String { get }
}

struct Color: Codable {
    var red : CGFloat = 0.0, green: CGFloat = 0.0, blue: CGFloat = 0.0, alpha: CGFloat = 0.0

    // retrieve UIColor from `Color`
    var uiColor : UIColor {
        return UIColor(red: red, green: green, blue: blue, alpha: alpha)
    }

    // assign red, green, blue, alpha from UIColor. name leave it nil
    init(uiColor : UIColor) {
        uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
    }
}

class BaseProps: Properties {
    // this class is prepared just for dynamic switching on decode
    var type: PropertyType
    var id, name: String
}

class NewsProps: Properties {

    // properties in common
    var type: PropertyType
    var id, name: String

    // properties only for this class
    var backgroundColor: Color
}

class GossipProps: Properties {
    var type: PropertyType
    var id, name: String

    var radius: CGFloat
    var backgroundColor: Color?
}

class TechProps: Properties {
    var type: PropertyType
    var id, name: String

    var refUrl: String
}

enum PropertyType: String, Codable {
    case News, Gossip, Tech

    var metatype: Properties.Type {
        switch self {
        case .News:
            return NewsProps.self
        case .Gossip:
            return GossipProps.self
        case .Tech:
            return TechProps.self
        }
    }
}

let jsonStr = """
[{"id":"xxxxx","name":"today's News","properties":{"type":"News","id":"xxxxxx","name":"news title here","backgroundColor":{"alpha":0,"red":0,"green":0,"blue":0}}},{"id":"xxxxx","name":"weekly Gossip","properties":{"type":"Gossip","id":"xxxxxx","name":"gossip title here","radius":3.6}},{"id":"xxxxx","name":"Technology topic","properties":{"type":"Tech","id":"xxxxxx","name":"tech title here","refUrl":"https://xxx"}}]
"""

let jsonData = jsonStr.data(using: .utf8)
do {
    let articles = try JSONDecoder().decode([Article].self, from:jsonData!)
    for article in articles {
        let mirror = Mirror(reflecting: article.properties)
        print(article.properties.self)
        print(mirror.children.compactMap { ($0.label ?? "", $0.value) })
    }
} catch let e {
    print(e)
}
```

## 補足

上記をそのままplaygroundに突っ込めば動くと思うので試してみてください。

### どうやってtypeでデコードを分岐させてるか

```swift
let properties = try BaseProps.init(from: container.superDecoder(forKey: .properties))
self.properties = try properties.type.metatype.init(from: container.superDecoder(forKey: .properties))
```

ここ↑で、一旦protocolに準拠しただけの`BaseProps`にデコードしておいて、`properties.type.metatype`で型を値として取得し、`init(from:)`でデコードします。

このやり方については、swift公式docの[Metatype Type](https://docs.swift.org/swift-book/ReferenceManual/Types.html#grammar_metatype-type)の項に記載されてますが、元々はこちらの記事をみつけて、踏襲してるだけです:innocent:
[Encoding And Decoding Polymorphic Objects In Swift](https://www.digitalflapjack.com/blog/2018/5/29/encoding-and-decoding-polymorphic-objects-in-swift)

### typeはenum
で定義しているので、例えばこうやってマッピングできる

```
case News = "news"
case Gossip = "gossip_news"
case Tech = "tech_news"
```

### BasePropsを継承しない理由

`BaseProps`は、どのクラスにも共通して存在するので、`NewsProps`等の具体的なクラスは`BaseProps`を継承すればいいじゃない、と思うかもしれませんが、そうすると、[子クラス固有のプロパティが自動ではデコードされない](https://qiita.com/_ha1f/items/bf1aad5ea3e927f59f9d#%E7%B6%99%E6%89%BF%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89)ので、いちいちCodingKeysとencode、initメソッドを用意しないといけないのでコードが冗長になってしまいます(多分2.5倍くらいのコード量)。なので、どのtypeもPropertiesを準拠する形で、継承関係を持たないようにしています。

今回、Mirrorで当該Codableのプロパティ一覧を取得したいという理由もあって、継承関係にすると面倒なので、Protocolに準拠する形で都合が良かったです。

### いい点

 - CodingKeysとかinit, encodeメソッドを用意するよりコード量は半分以下に抑えられると思う
 - protocolでメソッドを宣言して、その実装を各具体クラスに任せることができる(ポリモーフィズムれる)

### 注意点

今回は、判定したい型の種類が多くなる予定だったし、型によって個別の処理をしたいことがあったので、デコードする際に型を分けましたが、そもそもそこまでする必要無い場合もあると思います。そういうときは、**共通しないkeyをoptionalにすればいいだけ**なのでそのほうが楽だと思います。

例えばこういう感じ：

```swift:Articles.swift
// To parse the JSON, add this file to your project and do:
//
//   let articles = try? newJSONDecoder().decode(Articles.self, from: jsonData)

import Foundation

typealias Articles = [Article]

struct Article: Codable {
    let id, name: String
    let properties: Properties
}

struct Properties: Codable {
    let type, name: String
    let backgroundColor: BackgroundColor?
    let image: String?
    let radius: CGFloat?
}

struct BackgroundColor: Codable {
    let alpha, red, green, blue: CGFloat
}
```

これは、ほぼ[quicktype](https://app.quicktype.io/)で作ったものそのままなので、細かい事は置いといて、見てほしいのは、`struct Properties`で、`backgroundColor`と`image`と`radius`がoptionalになっているところ。

単純ですがこうすれば、型によってあったりなかったりするkey/valueも、optionalなので、Codableがよしなに処理してくれます。

ただ、後段で分岐が多くなったり、optionalをアンラップしないといけないのでコードが冗長になりやすかったりするので、その辺とのトレードオフかと思います。

他にも良いやり方とか、こうするといいよというのがあれば、やさしくコメントかedit request等もらえると嬉しいです。

## 参考

ほぼこの方のやり方を踏襲してます:relaxed: 
https://www.digitalflapjack.com/blog/2018/5/29/encoding-and-decoding-polymorphic-objects-in-swift

[wwdc2017 What's New in Foundation video](https://developer.apple.com/videos/play/wwdc2017/212/?time=1425)

[official swift docs "Encoding and Decoding Custom Types"](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)

[Codableについて色々まとめた[Swift4.x]](https://qiita.com/_ha1f/items/bf1aad5ea3e927f59f9d)