Codableで色々なJSONに対応する

元のJSONの構造のまま利用できればいいけど、構造を変えようと思うと結構コード量が増えてくる。
WebAPIのレスポンスを利用するだけなら`Decodable`に準拠するだけで十分だと思いました。

サンプルコードはすべてPlaygroundで実行できます。

### 関連記事を書きました

- [CodableでのValueObject利用](https://qiita.com/Mt-Hodaka/items/5e11dcbfb04040fa685f)

## 基本

### Codableに準拠していて、プロパティに使える型
`Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64`, `Int`, `Double`, `String`

あとは上記の型を要素に持つ`Optional`, `Array`, `Dictionary`

```swift
let data = """
{
    "model": "iPhone X",
    "displaySize": 5.8,
    "capacities": [64, 256],
    "biometricsAuth": "Face ID"
}
""".data(using: .utf8)!


struct Device: Codable {
    var model: String
    var displaySize: Float
    var capacities: [Int]
    var biometricsAuth: String? // nullの場合がある or キーがない場合がある
}


let device = try? JSONDecoder().decode(Device.self, from: data)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // リーダブルな出力
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
```

```ルートがArrayのJSON.swift
let list = """
[
    {
        "model": "iPhone 3G",
        "displaySize": 3.5,
        "capacities": [8, 16],
        "biometricsAuth": null
    },
    {
        "model": "iPhone 4",
        "displaySize": 3.5,
        "capacities": [8, 16, 32]
    }
]
""".data(using: .utf8)!


let devices = try? JSONDecoder().decode([Device].self, from: list)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(devices)
print(String(data: encoded, encoding: .utf8)!)
```

## 値にenumを使ったり、structをネストさせる

enumを利用する場合、`RawRepresentable`に準拠していて`RawValue`が
> `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64`, `Int`, `Double`, `String`

であれば、[init(from:)](https://developer.apple.com/documentation/swift/decodable/2894081-init), [encode(to:)](https://developer.apple.com/documentation/swift/encodable/2893603-encode) のデフォルト実装が用意されてるので簡単。

```swift
let data = """
{
    "model": "iPhone X",
    "capacities": [64, 256],
    "size": {
        "height": 143,
        "width": 70,
        "depth": 7
    }
}
""".data(using: .utf8)!


struct Device: Codable {
    var model: Model
    var capacities: [Capacity]
    var size: Size
    
    enum Model: String, Codable {
        case iPhoneX = "iPhone X"
        case iPhone8 = "iPhone 8"
        case iPhone8Plus = "iPhone 8 Plus"
    }
    
    enum Capacity: Int, Codable {
        case _64 = 64
        case _256 = 256
    }
    
    struct Size: Codable {
        var height: Int
        var width: Int
        var depth: Int
    }
}


let device = try? JSONDecoder().decode(Device.self, from: data)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
```

## enumのassociated valueを利用する

いずれかのキーでレスポンスが返ってくるみたいな場合に

```swift
// json1 か json2 どちらかの形式でレスポンスが返る想定
let jsonData: Data = {
    let json1 = """
    {"str": "文字列"}
    """
    
    let json2 = """
    {"num": 777}
    """

    return [json1, json2]
        .randomElement()!
        .data(using: .utf8)!
}()

enum Response: Codable {
    case str(String)
    case num(Int)
    
    private enum CodingKeys: String, CodingKey, CaseIterable {
        case str
        case num
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        if let value = try container.decodeIfPresent(String.self, forKey: .str) {
            self = .str(value)
        } else if let value = try container.decodeIfPresent(Int.self, forKey: .num) {
            self = .num(value)
        } else {
            throw DecodingError.dataCorrupted(.init(codingPath: CodingKeys.allCases,
                                                    debugDescription: "Does not match any CodingKey."))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        switch self {
        case .str(let value):
            try container.encode(value, forKey: .str)
        case .num(let value):
            try container.encode(value, forKey: .num)
        }
    }
}

let decoder = JSONDecoder()
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

do {
    let response = try decoder.decode(Response.self, from: jsonData)
    let encoded = try encoder.encode(response)
    print(String(data: encoded, encoding: .utf8)!)
} catch {
    dump(error)
}
```

## Date型のフォーマット

`JSONDecoder`は`dateDecodingStrategy`プロパティ, `JSONEncoder`は`dateEncodingStrategy`プロパティでDate型のパース方法を指定できる。

- [JSONDecoder.DateDecodingStrategy](https://developer.apple.com/documentation/foundation/jsondecoder.datedecodingstrategy)
- [JSONEncoder.DateEncodingStrategy](https://developer.apple.com/documentation/foundation/jsonencoder.dateencodingstrategy)

```swift
let data = """
{
    "model": "iPhone X",
    "releaseDate": "2017-10-19T11:53:36Z"
}
""".data(using: .utf8)!


struct Device: Codable {
    var model: String
    var releaseDate: Date
}


let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let device = try? decoder.decode(Device.self, from: data)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.dateEncodingStrategy = .iso8601
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
```

## 型変換

JSONで数値の文字列`"123"`が返ってくるが、Swiftでは`Int`で扱いたい場合など

```swift
let data = """
{
    "model": "iPhone X",
    "capacity": "64"
}
""".data(using: .utf8)!


struct Device: Codable {
    var model: String
    var capacity: Int
    
    private enum CodingKeys: String, CodingKey {
        case model
        case capacity
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        model = try values.decode(String.self, forKey: .model)
        capacity = Int(try values.decode(String.self, forKey: .capacity)) ?? 0
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(model, forKey: .model)
        try container.encode(capacity.description, forKey: .capacity)
    }
}


let device = try? JSONDecoder().decode(Device.self, from: data)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
```

## JSONのkeyと、structのpropertyのマッピング

```swift
let data = """
{
    "model_name": "iPhone X"
}
""".data(using: .utf8)!


struct Device: Codable {
    var modelName: String
    
    private enum CodingKeys: String, CodingKey {
        case modelName = "model_name"
    }
}


let device = try? JSONDecoder().decode(Device.self, from: data)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
```

Swift 4.1から、上記のようなスネークケース、キャメルケース変換は、`JSONDecoder`、`JSONEncoder`のプロパティを使うことでも対応できます。

```swift
let data = """
{
    "model_name": "iPhone X"
}
""".data(using: .utf8)!


struct Device: Codable {
    var modelName: String
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // スネークケースからの変換指定
let device = try? decoder.decode(Device.self, from: data)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.keyEncodingStrategy = .convertToSnakeCase // スネークケースへの変換指定
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
```
## ネストしたJSON <=> Flatな構造体

```swift
let data = """
{
    "model": "iPhone X",
    "specs": {
        "color": "Space Gray",
        "capacity": 64
    }
}
""".data(using: .utf8)!


struct Device: Codable {
    var model: String
    var color: Color
    var capacity: Int
    
    enum Color: String, Codable {
        case spaceGray = "Space Gray"
        case silver = "Silver"
    }
    
    private enum CodingKeys: String, CodingKey {
        case model
        case specs
    }
    
    private enum SpecsKeys: String, CodingKey {
        case color
        case capacity
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        model = try values.decode(String.self, forKey: .model)
        
        let specs = try values.nestedContainer(keyedBy: SpecsKeys.self, forKey: .specs)
        color    = try specs.decode(Color.self, forKey: .color)
        capacity = try specs.decode(Int.self, forKey: .capacity)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(model, forKey: .model)
        
        var specs = container.nestedContainer(keyedBy: SpecsKeys.self, forKey: .specs)
        try specs.encode(color, forKey: .color)
        try specs.encode(capacity, forKey: .capacity)
    }
}


let device = try? JSONDecoder().decode(Device.self, from: data)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(device)
print(String(data: encoded, encoding: .utf8)!)
```

## FlatなJSON <=> ネストした構造体

```swift
import Foundation

let data = """
{
    "model": "iPhone X",
    "height": 143,
    "width": 70,
    "depth": 7
}
""".data(using: .utf8)!


struct Device: Codable {
    var model: String
    var size: Size
    
    struct Size: Codable {
        var height: Int
        var width: Int
        var depth: Int
    }
    
    private enum CodingKeys: String, CodingKey {
        case model
        case height
        case width
        case depth
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        model = try values.decode(String.self, forKey: .model)
        size = try Size(from: decoder)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(model, forKey: .model)
        try size.encode(to: encoder)	
    }
}

let decoder = JSONDecoder()
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

do {
    let device = try JSONDecoder().decode(Device.self, from: data)
    dump(device)
    
    let encoded = try encoder.encode(device)
    print(String(data: encoded, encoding: .utf8)!)
} catch {
    dump(error)
}
```

## RawRepresentableを利用して苦しいJSONに対応

サイズの範囲情報が `_` 区切りで表現されているJSONにどう対応するかを考えてみます。

```json
{
    "size": "55.0_61.5"
}
```

Swiftの世界ではこのように扱いたいですね。

```swift
struct Size {
    var min: Double
    var max: Double
}
```

RawRepresentableを利用します。

```swift
let json = """
{
    "size": "55.0_61.5"
}
""".data(using: .utf8)!

struct Parameter: Codable {
    var size: Size
}

struct Size: Codable {
    var min: Double
    var max: Double
}

extension Size: RawRepresentable {
    init?(rawValue: String) {
        let separated = rawValue.components(separatedBy: "_")
        guard
            separated.count == 2,
            let min = separated.first.flatMap(Double.init),
            let max = separated.last.flatMap(Double.init)
            else { return nil }
        self.init(min: min, max: max)
    }
    
    var rawValue: String {
        return [String(min), String(max)].joined(separator: "_")
    }
}

let decoder = JSONDecoder()
let decoded = try! decoder.decode(Parameter.self, from: json)
dump(decoded)
//  __lldb_expr_11.Parameter
//    size: __lldb_expr_11.Size
//    - min: 55.0
//    - max: 61.5

let encoder = JSONEncoder()
let encoded = try! encoder.encode(decoded)
print(String(data: encoded, encoding: .utf8)!)
// {"size":"55.0_61.5"}
```

## 参考
[Codable - Swift Standard Library | Apple Developer Documentation](https://developer.apple.com/documentation/swift/codable)
[Swift4のJSONDecorderは、Date等のパース方法をカスタマイズできるみたい - Qiita](https://qiita.com/takasek/items/b2b0e3849e204037d2de)
[Codable in Swift 4.0 – Sarun W. – Medium](https://medium.com/@sarunw/codable-in-swift-4-0-1a12e38599d8)