Encode & Decodeの基本

iOSアプリなどのクライアントサイドからサーバーサイドにデータを送信する際、そのデータをサーバーに送信でき、かつサーバーで読み取れる値(多くの場合JSONが用いられます)に変換する必要があります。
Swiftでは、Swiftでのデータの型をサーバーに送信できる形へ変換することを`encode`、逆にサーバーから受け取った値をクライアント側で利用できる形に変換することを`decode`と呼びます。
そうしたencodeやdecodeをするために必要となる知識について解説していきます。

## Encode/Decode用のprotocol
Swiftでは、`Encodable`、`Decodable`と呼ばれるprotocolが存在しており、それぞれencode、decodeを行うことができるという性質を持っています。
ただし、encodeができるということはdecodeもできるといったことが多いため(片方だけ可能な意味があまりない)、`Encodable`、`Decodable`両方に準拠する`typealias`である`Codable`がよく用いられます。
以下のように、`PurchaseData`をサーバーに送信したいとなった場合には、下層`struct`も合わせて全て`Codable`にする必要があります。
```swift
struct PurchaseData: Codable {
    let user: ConsumerModel
    let price: Int
    let item: String
    let count: Int
}

struct ConsumerModel: Codable {
    let id: String
    let name: String

    init(name: String) {
        self.id = UUID().uuidString
        self.name = name
    }
}
```
これで、`PurchaseData`に対してencode、decodeとも行えるようになりました。

## encode, decodeを行う
SwiftではJSONにencode, decodeするための`class`が用意されています。
`JSONEncoder`、`JSONDecoder`です。
下記のようにインスタンス化してencodeすると、SwiftのData型となってしまうため直接`print`して中身を見ることはできなくなりますが、以下のように`String`のイニシャライザを使うことによって可視化することができます。
```swift
let user = ConsumerModel(name: "Taro")
let data1 = PurchaseData(user: user,
                         price: 200,
                         item: "toy",
                         count: 2)
let encoder = JSONEncoder()
if let json = try? encoder.encode(data1) {
    print(String(data: json,
                 encoding: .utf8)) // Optional("{\"user\":{\"id\":\"CCE0E7C8-B0DB-40D0-A268-4677991A9A14\",\"name\":\"Taro\"},\"item\":\"toy\",\"count\":2,\"price\":200}")
    let decoder = JSONDecoder()
    print(try? decoder.decode(PurchaseData.self,
                              from: json)) // Optional(PurchaseData(user: ConsumerModel(id: "CCE0E7C8-B0DB-40D0-A268-4677991A9A14", name: "Taro"), price: 200, item: "toy", count: 2))
}
```
出力結果を見ていただくと、encodeしたものはjsonの形になり、decodeしたものはdecodeの引数に指定した構造体のインスタンスになっていることがわかると思います。

## 様々な場合のencode, decode
### サーバーとkeyが異なる場合
例えば、`consumer`というキーでユーザー情報が渡さる場合など、キーがお互いに違う場合は、以下のように`CodingKeys`を使います。
キーが一致する場合も、下記のようにcaseとして記述する必要があります。
```swift
struct PurchaseData: Codable {
    let user: ConsumerModel
    let price: Int
    let item: String
    let count: Int

    // 追加
    enum CodingKeys: String, CodingKey {
        case user = "consumer"
        case price
        case item
        case count
    }
}
let user = ConsumerModel(name: "Taro")
let data1 = PurchaseData(user: user,
                         price: 200,
                         item: "toy",
                         count: 2)
let encoder = JSONEncoder()
if let json = try? encoder.encode(data1) {
    print(String(data: json,
                 encoding: .utf8)) // Optional("{\"price\":200,\"consumer\":{\"name\":\"Taro\",\"id\":\"22434D71-59B8-4970-8D70-DBA7C7183791\"},\"item\":\"toy\",\"count\":2}") 
                                   // キーが更新されている。
}
```
### サーバーとネストの階層が異なる場合
例えば以下のように、CustomerModelを直接渡す代わりにCustomerModelのnameのみ、他パラメータと同じ階層から送信したい場合です。
```swift
enum CodingKeys: String, CodingKey {
    case consumerName = "consumer_name"
    case price
    case item
    case count
}
```
その場合は以下のように、encodeとdecodeのための関数をカスタムで定義してあげる必要があります。
```swift
struct PurchaseData: Codable {
    let user: ConsumerModel
    let price: Int
    let item: String
    let count: Int

    // こちらも追加する必要がある
    init(user: ConsumerModel, 
         price: Int,
         item: String,
         count: Int) {
        self.user = user
        self.price = price
        self.item = item
        self.count = count
    }

    enum CodingKeys: String, CodingKey {
        case consumerName = "consumer_name"
        case price
        case item
        case count
    }

    // 追加
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(price, forKey: .price)
        try container.encode(item, forKey: .item)
        try container.encode(count, forKey: .count)
        try container.encode(user.name, forKey: .consumerName)
    }

    // 追加
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        price = try values.decode(Int.self, forKey: .price)
        item = try values.decode(String.self, forKey: .item)
        count = try values.decode(Int.self, forKey: .count)
        user = ConsumerModel(name: try values.decode(String.self, forKey: .consumerName))
    }
}

let data1 = PurchaseData(user: user,
                         price: 200,
                         item: "toy",
                         count: 2)
let encoder = JSONEncoder()
if let json = try? encoder.encode(data1) {
    print(String(data: json,
                 encoding: .utf8)) // Optional("{\"consumer_name\":\"Taro\",\"item\":\"toy\",\"price\":200,\"count\":2}")
    let decoder = JSONDecoder()
    print(try? decoder.decode(PurchaseData.self,
                              from: json)) // Optional(PurchaseData(user: ConsumerModel(name: "Taro"), price: 200, item: "toy", count: 2))
}
```
ここで、通常のinitializerも定義しないと、`init(from decoder: Decoder)`が呼ばれてしまうため、インスタンス化する部分でコンパイルエラーが発生してしまいます。注意してください。
### nilの場合、サーバーに送るデータに含めたくない場合
例えば以下のように、空欄でも良い`address`を入れる場合を考えます。
```swift
struct PurchaseData: Codable {
    let user: ConsumerModel
    let price: Int
    let item: String
    let count: Int
    // 追加
    let address: String?

    init(user: ConsumerModel, 
         price: Int,
         item: String,
         count: Int,
         address: String? = nil) {
        self.user = user
        self.price = price
        self.item = item
        self.count = count
        self.address = address
    }

    enum CodingKeys: String, CodingKey {
        case consumerName = "consumer_name"
        case price
        case item
        case count
        case address
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(price, forKey: .price)
        try container.encode(item, forKey: .item)
        try container.encode(count, forKey: .count)
        try container.encode(user.name, forKey: .consumerName)
        try container.encode(address, forKey: .address)
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        price = try values.decode(Int.self, forKey: .price)
        item = try values.decode(String.self, forKey: .item)
        count = try values.decode(Int.self, forKey: .count)
        user = ConsumerModel(name: try values.decode(String.self, forKey: .consumerName))
        address = try values.decode(String?.self, forKey: .address)
    }
}
let encoder = JSONEncoder()
if let json = try? encoder.encode(data1) {
    print(String(data: json,
                 encoding: .utf8)) // Optional("{\"address\":null,\"price\":200,\"item\":\"toy\",\"consumer_name\":\"Taro\",\"count\":2}")
}
```
`address`がnullでencodeされてしまうので、存在しないなら含めないようにしたい場合があると思います。そのような場合、以下のように`encodeIfPresent`, `decodeIfPresent`を使います。
```swift
struct PurchaseData: Codable {
    ...
    func encode(to encoder: Encoder) throws {
        ...
        // 修正
        try container.encodeIfPresent(address, forKey: .address)
    }

    init(from decoder: Decoder) throws {
        ...
        // 修正
        address = try values.decodeIfPresent(String.self,
                                             forKey: .address)
    }
}

let data1 = PurchaseData(user: user,
                         price: 200,
                         item: "toy",
                         count: 2)
let data2 = PurchaseData(user: user,
                         price: 200,
                         item: "toy",
                         count: 2,
                         address: "Japan, Shinjuku")
let encoder = JSONEncoder()
if let json = try? encoder.encode(data1) {
    print(String(data: json,
                 encoding: .utf8)) // Optional("{\"item\":\"toy\",\"count\":2,\"price\":200,\"consumer_name\":\"Taro\"}")
}
if let json2 = try? encoder.encode(data2) {
    print(String(data: json2,
                 encoding: .utf8)) // Optional("{\"item\":\"toy\",\"count\":2,\"address\":\"Japan, Shinjuku\",\"price\":200,\"consumer_name\":\"Taro\"}")
}
```
これで、`address`が存在する場合のみ、json中に含まれるようになりました。
## まとめ
- iOSからデータをサーバーに送信する際、データは`Encodable`に準拠し、またサーバーからデータを受け取る際、そのデータは`Decodable`に準拠している必要がある。
  - 両方を合わせたtypealiasはCodableと呼ばれる。
- サーバー側とiOS側で扱うパラメータのKeyが異なる場合、`CodingKeys`というenumを別途定義する必要がある。
- サーバー側とiOS側で扱うデータのネストが異なる場合、自前で`encode`関数、`init(from decoder: Decoder)`関数を定義する必要がある。
- nilのデータはjsonに含めない、といった処理は、`encodeIfPresent`/`decodeIfPresent`を定義することで解決できる。

### 最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。