Swift4で新しく追加されたCodable
を利用して、ルートが配列のJSONをそのままCodable
を適用したモデルの配列にマッピングするのではなく、該当する配列をプロパティとして持つCodable
を適用したコンテナ的なモデルにマッピングする方法が見つからず、色々調べた結果、対応方法がわかったので記しておく。
ルートが配列のJSON
例えばGithub APIのGet all usersのレスポンスは
[
{
"login": "mojombo",
"id": 1,
"url": "https://api.github.com/users/mojombo",
...
"site_admin": false
},
{
"login": "defunkt",
"id": 2,
"url": "https://api.github.com/users/defunkt",
...
"site_admin": true
},
...
]
とJSONのルートが配列になっている。
モデル
上記のJSONをアプリで利用するために、Codable
を適用したUser
モデルを以下のように作成する。
struct User: Codable {
let id: Int
let login: String
let url: String
}
デコード
単純にこのモデルオブジェクトの配列にデコードするのであれば、
let users: [User] = try JSONDecoder().decode([User].self, from: data)
で可能。
コンテナモデル
上記のように配列にデコードしたモデルをアプリ内で受け渡しながら使いまわすよりも、その配列をプロパティとして持つモデルにマッピングしたいことがある。
例えば以下のようなUserResponse
というモデル定義する。
struct UserResponse {
let users: [User]
}
しかし、このUserResponse
にCodable
を適用したもうまくいかない。
struct UserResponse: Codable {
let users: [User]
}
let response: UserResponse = try JSONDecoder().decode(UserResponse.self, from: data)
// typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead."))
カスタムデコード、エンコードメソッド
そこでDecodable
やEncodable
のメソッドを独自に実装する。
public protocol Decodable {
public init(from decoder: Decoder) throws
}
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
Decodable
extension UserResponse: Decodable {
init(from decoder: Decoder) throws {
var users: [User] = []
var unkeyedContainer = try decoder.unkeyedContainer()
while !unkeyedContainer.isAtEnd {
let user = try unkeyedContainer.decode(User.self)
users.append(user)
}
self.init(users: users)
}
}
Encodable
extension UserResponse: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for user in users {
try container.encode(user)
}
}
}
上記のようにDecodable
とEncodable
を実装すればデコード、エンコードともに期待したように動く。
// decode
let response: UserResponse = try JSONDecoder().decode(UserResponse.self, from: data)
// encode
let encoded = try JSONEncoder().encode(response)
let json = String(data: encoded, encoding: .utf8)