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
にする必要があります。
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
のイニシャライザを使うことによって可視化することができます。
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
を定義することで解決できる。
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。