0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Encode & Decodeの基本

Posted at

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

Encode/Decode用のprotocol

Swiftでは、EncodableDecodableと呼ばれるprotocolが存在しており、それぞれencode、decodeを行うことができるという性質を持っています。
ただし、encodeができるということはdecodeもできるといったことが多いため(片方だけ可能な意味があまりない)、EncodableDecodable両方に準拠するtypealiasであるCodableがよく用いられます。
以下のように、PurchaseDataをサーバーに送信したいとなった場合には、下層structも合わせて全てCodableにする必要があります。

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が用意されています。
JSONEncoderJSONDecoderです。
下記のようにインスタンス化してencodeすると、SwiftのData型となってしまうため直接printして中身を見ることはできなくなりますが、以下のようにStringのイニシャライザを使うことによって可視化することができます。

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として記述する必要があります。

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のみ、他パラメータと同じ階層から送信したい場合です。

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

その場合は以下のように、encodeとdecodeのための関数をカスタムで定義してあげる必要があります。

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を入れる場合を考えます。

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を使います。

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を定義することで解決できる。

最後に

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?