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)
}
}
}