0

I am very new to Swift and I am trying to create a tree of classes to model my data. I want to use JSONEncoder and JSONDecoder to send and receive objects. I have an issue when decoding generic classes inside another object (nested) because in the init(from: decoder) method I do not have access to other properties that could help me.

In my code:

NestedSecondObject extends NestedObjects, which extends Codable - NestedObject can be extended by NesteThirtObject and so on...

Contact extends Object1, which extends Codable; Contact contains a NestedObject type (which can be any subclass of NestedObject at runtime)

Because JSONEncoder and JSONDecoder do not support inheritance by default, i override the methods "encode" and init(from: decoder) as described here: Using Decodable in Swift 4 with Inheritance

My code is:

class NestedSecondObject: NestedObject {
    var subfield2: Int?

    private enum CodingKeys : String, CodingKey {
        case subfield2
    }

    override init() { super.init() }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(subfield2, forKey: .subfield2)
    }

    required init(from decoder: Decoder) throws
    {
        try super.init(from: decoder)
        let values = try decoder.container(keyedBy: CodingKeys.self)
        self.subfield2 = try values.decode(Int.self, forKey: .subfield2)

    }

}


class Contact:Object1 {
    var name: String = ""
    var age: Int = 0
    var address: String = ""
//    var className = "biz.ebas.platform.generic.shared.EContactModel"
    var nestedObject:NestedObject?

    private enum CodingKeys : String, CodingKey {
        case name,age,address,nestedObject
    }

    override init() { super.init() }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(address, forKey: .address)
        try container.encode(nestedObject, forKey: .nestedObject)
    }

    required init(from decoder: Decoder) throws
    {
        try super.init(from: decoder)
        print(type(of: self))
        print(type(of: decoder))

        let values = try decoder.container(keyedBy: CodingKeys.self)
        print(type(of: values))


        self.name = try values.decode(String.self, forKey: .name)
        self.age = try values.decode(Int.self, forKey: .age)
        self.address = try values.decode(String.self, forKey: .address)
        self.nestedObject = try values.decodeIfPresent(???.self, forKey: .nestedObject)  // HERE i need to know what ???.self is
    }
}

Decoding is:

let jsonDec = JSONDecoder()
let jsonData = json?.data(using: .utf8)
let decodedContact: Contact = try jsonDec.decode(Contact.self, from: jsonData!)

So, basically, when I make a request to the server, I know what types I receive (let's say I request NestedSecondObject), but how do I pass it to the method "init(from decoder:Decoder)" ?

I tried to extend class JSONDecoder and add a simple property like this:

class EJSONDecoder: JSONDecoder {

    var classType:AnyObject.Type?

}

But inside the required init(from decoder:Decoder) method, the type of decoder is not EJSONDecoder, is _JSONDecoder, so I cannot access the decoder.classType property.

Can anyone help with a solution or some sort of workaround? Thanks!

Andrei F
  • 4,205
  • 9
  • 35
  • 66
  • Generally I'd say you have to inspect the "raw" JSON data of the nested object to figure out what type it is, then use this type for decoding. – dr_barto Jul 26 '18 at 13:28

1 Answers1

1

New answer

You can give the decoder a userInfo array where you can store things you want to use during decoding.

let decoder = JSONDecoder()
decoder.userInfo = [.type: type(of: NestedSecondObject.self)]
let decodedContact: Contact = try! decoder.decode(Contact.self, from: json)

extension CodingUserInfoKey {
    static let type = CodingUserInfoKey(rawValue: "Type")!
}

Then use it during decoding:

switch decoder.userInfo[.type]! {
case let second as NestedSecondObject.Type:
    self.nestedObject = try values.decode(second, forKey: .nestedObject)
case let first as NestedObject.Type:
    self.nestedObject = try values.decode(first, forKey: .nestedObject)
default:
    fatalError("didnt work")
}

I have sadly not found a way to skip the switch.

Old answer:

Decode as

NestedSecondObject.self

and if that fails decode inside the catch with

NestedObject.self

. Do not catch the NestedObject-decoding cause you want to fail if its not even decodable to the basic type.

Fabian
  • 5,040
  • 2
  • 23
  • 35
  • The problem is that I do not know that NestedSecondClass is the actual class. In the end, I think i will send the class in the json string response from the server – Andrei F Jul 26 '18 at 16:59
  • 1
    @AndreiF best use the [.userInfo](https://forums.swift.org/t/codable-passing-data-to-child-decoder/12757/2) property of the decoder if you don’t want to try through all the possible types it can be. – Fabian Jul 26 '18 at 17:19
  • @AndreiF I updated the answer with an example how you can apply userInfo to do what you want. – Fabian Jul 27 '18 at 15:20
  • That is what I did also. But I don't understand, why can't I use this? : self.nestedObject = try values.decode(decoder.userInfo[.type]!, forKey: .nestedObject) - I mean, how can I avoid the switch / if statements? – Andrei F Jul 27 '18 at 16:09
  • Since its a generic function decode probably has to know at compile time what it is supposed to return. It can’t get that information from the argument since it can be any type. And it wouldn’t know the given type is decodable in the first place, and it has to know that at compile time. – Fabian Jul 27 '18 at 16:24