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
    Have a look at stackoverflow.com/questions/46344963/…. – Martin R Dec 1 '17 at 9:50
  • This link is really helpful. Thanks! – kelin Dec 1 '17 at 10:01
  • 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 – muescha Dec 9 '17 at 19:33

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

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.