とあるAPIのレスポンスをデコードする処理を書いていて、以下のように配列の中の型が複数あるケースに対応する必要がありました。
{
"products": [
{
"id": 1,
"type": "FOO",
"foo": "foofoo",
"bar": "barbar"
},
{
"id": 2,
"type": "BAZ",
"baz": "bazbaz",
"qux": "quxqux"
}
],
"next": "xxxxxxx"
}
id
とtype
のように、一部共通のプロパティも持っていて、これらを抽象的な型でまとめて表現したいというモチベーションもありました。
参考になる(というかほぼそのまま使える)コードをStackOverflowで見つけてなんとか実現できたので、その方法をご紹介したいと思います。
やりたいこと
先ほどのJSONを以下のような型にデコードしたいです。
struct Response: Decodable {
let products: [Product]
let next: String?
}
protocol Product: Decodable {
var id: Int { get }
var type: ProductType { get }
}
enum ProductType: String, Decodable {
case foo = "FOO"
case baz = "BAZ"
}
struct ProductFoo: Product {
let id: Int
let type: ProductType
let foo: String
let bar: String
}
struct ProductBaz: Product {
let id: Int
let type: ProductType
let baz: String
let qux: String
}
標準のDecodableだけだとコンパイルエラーになる
上記の宣言をしても、このままだと以下のエラーが出てコンパイルできません。ProductがProtocolとして宣言されていて、具体的な型が確定できないからですね。
Type 'Response' does not conform to protocol 'Decodable'
自動でDecodableに準拠できないのであれば、自前でinit(from:)
を実装して準拠させてみましょう。配列の中が任意のJSONになるのであれば、一旦[Any]
としてデコードして、あとでProductFooかProductBazに変換してあげれば良いと考えました。
struct Response: Decodable {
let products: [Product]
let next: String?
enum CodingKeys: String, CodingKey {
case products
case next
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.next = try container.decode(String?.self, forKey: .next)
let anyProducts = try container.decode([Any].self, forKey: .products) // コンパイルエラー
...
}
}
しかし、これでもコンパイルエラーになります。
No exact matches in call to instance method 'decode'
container
はKeyedDecodingContainerという標準ライブラリが提供する構造体なのですが、こいつに[Any]
にデコードするメソッドが定義されていないため、コンパイルエラーになってしまいます。
[Any]でデコードできるようにする
どうしたものか困っていたところ、ドンピシャなStackOverflowの回答を見つけました。
この回答にリンクされているGistのコードを導入すると、[Any]
もしくは[String: Any]
でデコードできるようになります。
若干タイポがあったりするので、ちょっと手直ししたコードがこちらです。
struct JSONCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
これで[String: Any]
もしくは[Any]
でデコードできるようになります。
[Any]でデコードしたあと任意の型に変換する
init(from:)
の実装の完全版はこちらです。
struct Response: Decodable {
let products: [Product]
let next: String?
enum CodingKeys: String, CodingKey {
case products
case next
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.next = try container.decode(String?.self, forKey: .next)
// [1]
let anyProducts = try container.decode([Any].self, forKey: .products)
self.products = anyProducts.compactMap {
// [2]
guard let data = try? JSONSerialization.data(withJSONObject: $0, options: []) else { return nil }
// [3]
if let product = try? JSONDecoder().decode(ProductFoo.self, from: data) {
return product
} else if let product = try? JSONDecoder().decode(ProductBaz.self, from: data) {
return product
} else {
return nil
}
}
}
}
[1]
products
の配列は一旦[Any]
としてデコードします。
[2]
JSONSerializationでData型に変換します。
[3]
JSONDecoderで具体的な型に変換します。try?で変換処理を実行し、型が合わなければelse ifでつなげて別の型で変換を実行します。
任意の型にデコードできるようになった
最初のJSONはこんな感じで変換できるようになりました。products
内はProductFooもしくはProductBazという型に変換され、またProduct型として共通の振る舞いを持つこともできています。
let json = """
{
"products": [
{
"id": 1,
"type": "FOO",
"foo": "foofoo",
"bar": "barbar"
},
{
"id": 2,
"type": "BAZ",
"baz": "bazbaz",
"qux": "quxqux"
}
],
"next": "xxxxxxx"
}
"""
let decoder = JSONDecoder()
let response = try! decoder.decode(Response.self, from: json.data(using: .utf8)!)
let foo = response.products[0] as! ProductFoo
let baz = response.products[1] as! ProductBaz
print(foo.id) // 1
print(foo.type) // foo
print(foo.foo) // foofoo
print(foo.bar) // barbar
print(baz.id) // 2
print(baz.type) // baz
print(baz.baz) // bazbaz
print(baz.qux) // quxqux
response.products.forEach { print("id: \($0.id), type: \($0.type)") }
// id: 1, type: foo
// id: 2, type: baz