How to use JSONDecoder with inheritance / protocols?

Viewed 797

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"
}
]
2 Answers

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

I've just found a nice solution to this problem.

Related