XMLCoder icon indicating copy to clipboard operation
XMLCoder copied to clipboard

Class vs Struct for Decoding

Open peterent opened this issue 3 years ago • 6 comments

Let's say I have this simple XML:

let sample = """
            <wrapper>
                <firstName>Peter</firstName>
                <lastName>Ent</lastName>
                <address>Atlanta, GA</address>
            </wrapper>
        """

And I wish to decode it into the following Swift classes:

class MyBase: Codable {
    var firstName: String?
    var lastName: String?
}

class MyAddress: MyBase {
    var address: String?
}

I've set up set up a function like this:

func decodeMessage<T: Codable>(_ data: Data) -> T? {
        let response = try? XMLDecoder().decode(T.self, from: data)
        return response
}

When I call it as follows:

let dataIn = sample.data(using: .utf8)!
if let person = parser.decodeMessage(dataIn) as MyAddress? {
        print("Decode works: \(String(describing: person.firstName)) \(String(describing: person.lastName)) at \(String(describing: person.address))")
}

It only parses the MyBase class (firstName and lastName), ignoring address in the MyAddress class. Why is that?

I can work around this by using CodingKeys and implementing encode and decode functions, but I shouldn't have to, right?

Or am I missing something?

peterent avatar Jan 15 '21 14:01 peterent

Unfortunately, you have to implement init(from decoder:) and encode functions manually when using classes with inheritance, this is how Codable works. This is also the case for JSONDecoder.

MaxDesiatov avatar Jan 15 '21 15:01 MaxDesiatov

A workaround could be to use composition instead of inheritance:

struct Person: Codable {
    let nameDetails: NameDetails
    let address: Address

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        nameDetails = try container.decode(NameDetails.self)
        address = try container.decode(Address.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(nameDetails)
        try container.encode(address)
    }
}

struct NameDetails: Codable {
    var firstName: String?
    var lastName: String?
}

struct Address: Codable {
    var address: String?
}

let personJSON = #"{ "firstName": "Peter", "address": "Atlanta, Georgia, USA" }"#.data(using: .utf8)!
let addressJSON = #"{ "address": "London, United Kingdom" }"#.data(using: .utf8)!

let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: personJSON)
let address = try! decoder.decode(Address.self, from: addressJSON)

This way you'd only need to maintain custom Codable implementation only for the base Person type, while any fields added to NameDetails or Address would be handled automatically. And you could use either NameDetails or Address instead of Person if you need to parse just corresponding subsets of fields. I'm using JSON as an example here, but I would expect it to work the same with XMLCoder.

@peterent does that help with your issue?

MaxDesiatov avatar Jan 15 '21 15:01 MaxDesiatov

Another option is to declare custom Codable conformance in your derived MyAddress class as shown in this SO answer. But you still have to tell the decoder that you're decoding MyAddress upfront, it won't be able to figure out whether to decode a base class or a derived class dynamically.

MaxDesiatov avatar Jan 15 '21 15:01 MaxDesiatov

@MaxDesiatov Another question related to Class vs Struct. I've got decoding working with a class but when i change that class to a struct, the decoding no longer works. Why would that be?

transat avatar Mar 30 '21 12:03 transat

It's very hard to say without looking at the actual code. Do you have an isolated reproducible test case for this?

MaxDesiatov avatar Mar 30 '21 12:03 MaxDesiatov

I'll try to set something up.

transat avatar Apr 01 '21 01:04 transat