3

Say I have a single array of employees and employers mixed together in a JSON. Both are inheriting from Person. What's the right way to handle it in JSONDecoder? Something like wouldn't work since we can't cast it back to a subclass:

let decoder = JSONDecoder()
let persons = try! decoder.decode([Person].self, for: jsonData)

And another point: can we use protocols instead of super classes here?

This is how my example JSON looks like:

[
{
    "id": 1,
    "type": "employee",
    "employee_name": "xy"
},
{
    "id": 2,
    "type": "employer",
    "employer_name": "xz"
}
]
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
cocoapriest
  • 1,869
  • 3
  • 22
  • 36
  • Related: [Encode/Decode Array of Types conforming to protocol with JSONEncoder](https://stackoverflow.com/q/44441223/2976878) – Hamish Nov 11 '17 at 16:28

1 Answers1

0

First of all lets transform your json in a Data value

let data = """
[
    {
        "id": 1,
        "type": "employee",
        "employee_name": "xy"
    },
    {
        "id": 2,
        "type": "employer",
        "employer_name": "xz"
    }
]
""".data(using: .utf8)!

Important: Replace the "!" with a safer unwrapping method

Model

Now we need a model value to mach the element in your JSON

struct ResponseElement:Codable {
    let id: Int
    let type: Type
    let employeeName: String?
    let employerName: String?

    enum CodingKeys: String, CodingKey {
        case id, type, employeeName = "employee_name", employerName = "employer_name"
    }

    enum `Type`:String, Codable {
        case employee, employer
    }
}

As you can see employeeName and employerName are optionals, so this struct will be able to hold every element of your JSON (Employers and Employee).

Person and its subclasses

Assuming you have a class Person like this

class Person {
    let id: Int
    let name:String

    init(id:Int, name:String) {
        self.id = id
        self.name = name
    }
}

You'll need to create the Employee and Empolyer subclasses like these

class Employee:Person {
    init?(responseElement:ResponseElement) {
        guard let name = responseElement.employeeName, responseElement.type == .employee else { return nil }
        super.init(id: responseElement.id, name: name)
    }
}
class Employer:Person {
    init?(responseElement:ResponseElement) {
        guard let name = responseElement.employerName, responseElement.type == .employer else { return nil }
        super.init(id: responseElement.id, name: name)
    }
}

Please note that an Employee has a failable initializer that will try to create an Employee starting ResponseElement. Same for Employer.

Lets decode!

do {
    let elements = try JSONDecoder().decode([ResponseElement].self, from: data)
    let employees = elements.filter { $0.type == .employee }.flatMap(Employee.init)
    let employers = elements.filter { $0.type == .employer }.flatMap(Employer.init)

    print(employees.count)
    print(employers.count)
} catch {
    print("Something bad has happened :\(error)")
}
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
  • hey Luca thanks for your solution. It would definitely work, but the ResponseElement seems to have too much of a knowledge of other model classes. Having only 2 models is not that of an issue, but having 5-10 models each having 5-10 fields would lead to a boilerplate code, IMO. See my answer for an interesting solution I've found on Github. – cocoapriest Nov 11 '17 at 16:15