Encodable
, Decodable
プロトコルと JSONEncoder
, JSONDecoder
を利用すれば、 HTTP 通信で取得した JSON と Swift オブジェクトを一発変換できます🙂Encodable
のメソッド encode(to:)
Decodable
のイニシャライザ init(from:)
{
"user_name": "山田二郎",
"scores": [
{ "score": 65 },
{ "score": 24 }
]
}
上記の JSON は、構造として全体を表す {}
の中に、 "scores"
部分が配列となっており、その要素が {}
となっています。
つまり、「オブジェクト」->「配列」->「オブジェクト」の 3 階層です。
配列は Swift で Array
型が定義されているので、自分で用意する必要があるのは 2 つの構造体であることがわかります。
この JSON の階層に単純に対応させるなら、以下のような 2 つの構造体が必要となります。
// JSONと対応させるPerson型(Codableに準拠)
struct Person: Codable {
let name: String
let scores: [Score]
/// SwiftのプロパティとJSONのキーをマッピング
enum CodingKeys: String, CodingKey {
// case Swift側の名前 = "JSON側のキー"
case name = "user_name"
case scores
}
}
// Personのプロパティとして利用する型(Codableに準拠)
struct Score: Codable {
let score: Int
}
しかし、 "scores"
部分は属性が "score"
しかないため、整数の配列にしておいた方が扱いやすそうです。
こんな感じで、 1 階層浅くしたら使いやすそうですね。
"scores"
は Int
の配列であるため、定義する型は Person
のみです。
上のサンプルにある、 Score
型は定義する必要はありません。
階層ごとの CodingKeys
だけは用意しておきましょう。名前は任意です。
// JSONに対応する構造体
struct Person: Codable {
let name: String
let scores: [Int]
// トップレベルの属性に対応するCodingKeys
enum CodingKeys: String, CodingKey {
case name = "user_name"
case scores
}
// ネストしたJSONの属性に対応するCodingKeys
enum ScoresCodingKeys: String, CodingKey {
case score
}
}
このままでは、 JSON と形が異なるため相互変換ができません。 エクステンションで、カスタムデコード用のイニシャライザとカスタムエンコード用のメソッドを追加してみましょう。
extension Person {
init(from decoder: Decoder) throws {
// CodingKeysを指定し、JSON直下の属性("user_name"と"scores"にあたる部分)に対するコンテナを取得
let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
// JSONのキー"user_name"にあたる部分の値を取得
let name = try rootContainer.decode(String.self, forKey: .name)
// ネストしたオブジェクト(キー"scores")の配列部分(配列なので中身の各要素にはキーがない)のコンテナを取得
var arrayContainer = try rootContainer.nestedUnkeyedContainer(forKey: .scores)
var scores: [Int] = []
// 配列の要素の最後になるまで繰り返し
while !arrayContainer.isAtEnd {
// ネストした部分のCodingKeys(ここではScoresCodingKeys)を指定し配列内のオブジェクト部分のコンテナを取得
let scoreContainer = try arrayContainer.nestedContainer(keyedBy: ScoresCodingKeys.self)
// JSONのキー"score"にあたる部分の値を取得
let score = try scoreContainer.decode(Int.self, forKey: .score)
// 取得した値を配列に追加
scores.append(score)
}
// 取得した値をメンバワイズイニシャライザに渡して初期化
self.init(name: name, scores: scores)
}
}
Decoder
を利用する
Decoder
を通して行いますDecoder
の container(keyedBy:)
に CodingKeys
を渡して、該当部分のコンテナを取得
CodingKeys
が JSON のキー(と、それに対応する構造体のプロパティ名)を保持しているため、そのコンテナを通して値を取得できるようになりますdecode(_:forKey:)
で CodingKeys
に定義したキーを渡して、該当部分の値を取得
container(keyedBy:)
で CodingKeys
が渡っているため、そこに定義したキーで値を取得できますnestedContainer(keyedBy:)
や nestedUnkeyedContainer(forKey:)
で取得
nestedContainer(keyedBy:)
を利用しますが、値が配列の場合には中の各要素にキーがないため、 nestedUnkeyedContainer(forKey:)
を利用しますnestedContainer(keyedBy:)
を呼び出すごとに、内部的なカーソルが次へ移動する
isAtEnd
を条件としてループを回せば、要素の回数だけループを回せますnestedContainer(keyedBy:)
を呼び忘れると無限ループに陥るので注意が必要ですextension Person {
// カスタムでエンコードするためのメソッド
func encode(to encoder: Encoder) throws {
// CodingKeysを指定し、JSON直下の属性("user_name"と"scores"にあたる部分)に対するコンテナを取得
var container = encoder.container(keyedBy: CodingKeys.self)
// JSONのキー"name"にあたる部分をエンコード
try container.encode(self.name, forKey: .name)
// ネストしたオブジェクト(キー"scores")の配列部分(配列なので中身の各要素にはキーがない)のコンテナを取得
var scoresContainer = container.nestedUnkeyedContainer(forKey: .scores)
// scores配列をループし、各要素をエンコード
for score in scores {
// ネストした部分のCodingKeys(ここではScoresCodingKeys)を指定し配列内のオブジェクト部分のコンテナを取得
var arrayContainer = scoresContainer.nestedContainer(keyedBy: ScoresCodingKeys.self)
// JSONのキー"score"にあたる部分をエンコード
try arrayContainer.encode(score, forKey: .score)
}
}
}