DevShark
DevShark

Reputation: 9112

How to create object from JSON in Swift?

I have N classes all deriving from Base :

class Base {...}

class A : Base {
   init(a:Int) {...}
}

class B : Base {
   init(b:Int, c:Int) {...}
}

They have been serialized into a json file that contains an array of Base. It looks like this:

{
"elements" : [
    {
    "type" : "A",
    "a" : 3
    },
    {
    "type" : "B",
    "b" : 30,
    "c" : 45
    }
  ]
}

When I decode the json, I have a dictionary like one of the 2 above. How can I then create an object of one of my N classes from it ?

Important note : for various reasons, I cannot use Codable, and JSONEncoder, JSONDecoder

Upvotes: 0

Views: 177

Answers (2)

Rob Napier
Rob Napier

Reputation: 299275

For classes, it's not quite as straightforward as for structs, but it's still pretty easy to implement this kind of Decodable.

struct ElementContainer: Decodable {
    let elements: [Base]
    enum CodingKeys: String, CodingKey { case elements }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.elements = try container.decode([Element].self, forKey: .elements)
            .map { $0.base }
    }
}

struct Element: Decodable {
    let base: Base
    enum CodingKeys: String, CodingKey {
        case type
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)

        switch type {
        case "A":
            base = try A(from: decoder)
        case "B":
            base = try B(from: decoder)

        default:
            throw DecodingError.dataCorruptedError(forKey: .type,
                                                   in: container,
                                                   debugDescription: "Unknown type: \(type)")
        }
    }
}

class Base {}

class A : Base, Decodable {
    let a: Int
    init(a:Int) {
        self.a = a
        super.init()
    }
}

class B : Base, Decodable {
    let b: Int
    let c: Int
    init(b:Int, c:Int) {
        self.b = b
        self.c = c
        super.init()
    }
}

let results = try JSONDecoder().decode(ElementContainer.self, from: json).elements

Structs are a little simpler and you can get rid of the switch statement. It's harder to do this with classes because it introduces required inits that are tedious to implement.

struct ElementContainer: Decodable {
    let elements: [Element]
}

struct Element: Decodable {
    let base: Base
    enum CodingKeys: String, CodingKey {
        case type
    }

    static let mapping: [String: Base.Type] = ["A": A.self, "B": B.self]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)

        guard let elementType = Element.mapping[type] else {
            throw DecodingError.dataCorruptedError(forKey: .type,
                                                   in: container,
                                                   debugDescription: "Unknown type: \(type)")
        }

        base = try elementType.init(from: decoder)
    }
}

protocol Base: Decodable {}

struct A : Base {
    let a: Int
}

struct B : Base {
    let b: Int
    let c: Int
}

Upvotes: 1

vadian
vadian

Reputation: 285069

As the JSON contains the type use it to determine the different classes

if let elements = root["elements"] as? [[String:Any]] {
    for element in elements {
        let type = element["type"] as! String
        switch type {
            case "A": 
              let a = element["a"] as! Int
              let aInstance = A(a: a)
            case "B": // decode b and c and create instance of B
            // and so on
        }
    }
}

Upvotes: 2

Related Questions