I need to parse a JSON generated by iTunes Search API. I'm looking for all apps by a certain developer, and I usually get a response like this (simplified):
{
"resultCount":4,
"results": [
{
"wrapperType":"artist",
"artistType":"Software Artist",
"artistName":"Vladimir Kelin"
},
{
"wrapperType":"software",
"formattedPrice":"Free",
"trackId":933241656
},
{
"wrapperType":"software",
"formattedPrice":"1ドル.99",
"trackId":1175807581
}
]
}
As you can see, the results
array is heterogenous, meaning it contains objects of 2 different types: artist
and software
.
I created a structure, implementing a new handy Codable
protocol:
struct App: Codable {
let trackId: Int
let formattedPrice: String?
/// - Parameter jsonDict: A dictionary produced by JSONSerialization
init?(jsonDict: [String: Any]) {
guard let trackId = jsonDict["trackId"] as? Int else { return nil }
self.trackId = trackId
formattedPrice = jsonDict["formattedPrice"] as? String
}
}
I don't want to parse artist
data, so I didn't created any structures for that.
It's relatively easy to parse such JSON using JSONSerialization
:
do {
let root = try unwrap(JSONSerialization.jsonObject(with: receivedData, options: []) as? [String: Any])
let results = try unwrap(root["results"] as? [[String: Any]])
var apps = [App]()
for result in results {
// Check "software" or "artist"
guard result["wrapperType"] as? String == "software" else { continue }
guard let app = App(jsonDict: result) else { continue }
apps.append(app)
}
completion(.ok, apps)
} catch let e {
let dataStr = String(bytes: receivedData, encoding: .utf8) ?? ""
print("Exception: \"\(e)\", while parsing data: \"\(dataStr)\"")
completion(.parseError, nil)
}
Do you see the unwrap()
function for the first time?
The Problem. I'm looking for a way to parse the response using wonderful JSONDecoder
class introduced in Swift 4. If the results
array were homogenous I would be able to parse it in just few strings of code.
// Helper struct
struct TunesResponse: Codable {
let resultCount: Int
let results: [App]
}
// Parsing
let jsonDecoder = JSONDecoder()
if let parsedResponse = jsonDecoder.decode(TunesResponse.self, from: receivedData) {
let apps = parsedResponse.results
completion(.ok, apps)
} else {
completion(.parseError, nil)
}
In practice I spend a lot of time but didn't found any way to apply JSONDecoder for such kind of response.
1 Answer 1
First of all, the models need to be defined, conforming to the Decodable protocol. Starting with the base class and then the subclass, see the example below:
class FieldDefinition : NSObject, Codable {
let type: String
enum CodingKeys: String, CodingKey {
case type
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.type, forKey: .type)
}
}
class TextFieldDefinition: FieldDefinition {
let maxLength: Int
enum TextCodingKeys: String, CodingKey {
case maxLength
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TextCodingKeys.self)
self.maxLength = try container.decode(Int.self, forKey: .maxLength)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: TextCodingKeys.self)
try container.encode(self.maxLength, forKey: .maxLength)
try super.encode(to: encoder)
}
}
And here comes the tricky part. Next we create an enum DemoClassKeys
, which is used to decode the dictionary containing the array of fields.
We also define an enum FieldDefinitionTypeKey
which is used to serialize the type field (wrapperType in your case) for each object in the array.
And a final enum FieldDefinitionTypes with specifies all the possible FieldDefinition types.
With all the enums specified we can get to the serialization :
class DemoClass: Decodable {
var fieldDefinitions: [FieldDefinition]
enum DemoClassKeys: String, CodingKey {
case fieldDefinitions
}
enum FieldDefinitionTypeKey: CodingKey {
case type
}
enum FieldDefinitionTypes: String, Decodable {
case textFieldDefinition = "TextFieldDefinition"
case deterministicFieldDefinition = "DeterministicFieldDefinition"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DemoClassKeys.self)
var arrayOfFieldType = try container.nestedUnkeyedContainer(forKey: DemoClassKeys.fieldDefinitions)
var fieldDefinitions = [FieldDefinition]()
var tmp = arrayOfFieldType
while(!arrayOfFieldType.isAtEnd) {
let fieldDefinition = try arrayOfFieldType.nestedContainer(keyedBy: FieldDefinitionTypeKey.self)
let type = try fieldDefinition.decode(FieldDefinitionTypes.self, forKey: FieldDefinitionTypeKey.type)
switch type {
case .textFieldDefinition:
fieldDefinitions.append(try tmp.decode(TextFieldDefinition.self))
case .deterministicFieldDefinition:
fieldDefinitions.append(try tmp.decode(DeterministicFieldDefinition.self))
}
}
self.fieldDefinitions = fieldDefinitions
}
}
And Here is the full working Demo:
var json = """
{
"fieldDefinitions": [
{
"type": "TextFieldDefinition",
"maxLength": 5
},
{
"type": "DeterministicFieldDefinition",
"defaultValue": "VISA"
}
]
}
"""
let jsonDecoder = JSONDecoder()
do {
let results = try jsonDecoder.decode(DemoClass.self, from: json.data(using: .utf8)!)
for result in results.fieldDefinitions {
print(result.self)
}
} catch {
print("caught: \(error)")
}
-
\$\begingroup\$ Sorry but the code does not compile: DeterministicFieldDefinition is missing. \$\endgroup\$Nick Weaver– Nick Weaver2020年04月17日 18:37:30 +00:00Commented Apr 17, 2020 at 18:37
Artist
,App
) returned depending on thewrapperType
value \$\endgroup\$