3
\$\begingroup\$

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.

asked Dec 1, 2017 at 9:13
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Have a look at stackoverflow.com/questions/46344963/…. \$\endgroup\$ Commented Dec 1, 2017 at 9:50
  • \$\begingroup\$ i would like to see how to decode different types with JSONDecoder (like in your example different Objects (Artist, App) returned depending on the wrapperType value \$\endgroup\$ Commented Dec 9, 2017 at 19:33

1 Answer 1

3
\$\begingroup\$

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)")
}
Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
answered Apr 6, 2018 at 16:33
\$\endgroup\$
1
  • \$\begingroup\$ Sorry but the code does not compile: DeterministicFieldDefinition is missing. \$\endgroup\$ Commented Apr 17, 2020 at 18:37

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.