Using Decodable in Swift 4 with Inheritance

37,896

Solution 1

I believe in the case of inheritance you must implement Coding yourself. That is, you must specify CodingKeys and implement init(from:) and encode(to:) in both superclass and subclass. Per the WWDC video (around 49:28, pictured below), you must call super with the super encoder/decoder.

WWDC 2017 Session 212 Screenshot at 49:28 (Source Code)

required init(from decoder: Decoder) throws {

  // Get our container for this subclass' coding keys
  let container = try decoder.container(keyedBy: CodingKeys.self)
  myVar = try container.decode(MyType.self, forKey: .myVar)
  // otherVar = ...

  // Get superDecoder for superclass and call super.init(from:) with it
  let superDecoder = try container.superDecoder()
  try super.init(from: superDecoder)

}

The video seems to stop short of showing the encoding side (but it's container.superEncoder() for the encode(to:) side) but it works in much the same way in your encode(to:) implementation. I can confirm this works in this simple case (see playground code below).

I'm still struggling with some odd behavior myself with a much more complex model I'm converting from NSCoding, which has lots of newly-nested types (including struct and enum) that's exhibiting this unexpected nil behavior and "shouldn't be". Just be aware there may be edge cases that involve nested types.

Edit: Nested types seem to work fine in my test playground; I now suspect something wrong with self-referencing classes (think children of tree nodes) with a collection of itself that also contains instances of that class' various subclasses. A test of a simple self-referencing class decodes fine (that is, no subclasses) so I'm now focusing my efforts on why the subclasses case fails.

Update June 25 '17: I ended up filing a bug with Apple about this. rdar://32911973 - Unfortunately an encode/decode cycle of an array of Superclass that contains Subclass: Superclass elements will result in all elements in the array being decoded as Superclass (the subclass' init(from:) is never called, resulting in data loss or worse).

//: Fully-Implemented Inheritance

class FullSuper: Codable {

    var id: UUID?

    init() {}

    private enum CodingKeys: String, CodingKey { case id }

    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)

    }

    func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)

    }

}

class FullSub: FullSuper {

    var string: String?
    private enum CodingKeys: String, CodingKey { case string }

    override init() { super.init() }

    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)
        let superdecoder = try container.superDecoder()
        try super.init(from: superdecoder)

        string = try container.decode(String.self, forKey: .string)

    }

    override func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(string, forKey: .string)

        let superencoder = container.superEncoder()
        try super.encode(to: superencoder)

    }
}

let fullSub = FullSub()
fullSub.id = UUID()
fullSub.string = "FullSub"

let fullEncoder = PropertyListEncoder()
let fullData = try fullEncoder.encode(fullSub)

let fullDecoder = PropertyListDecoder()
let fullSubDecoded: FullSub = try fullDecoder.decode(FullSub.self, from: fullData)

Both the super- and subclass properties are restored in fullSubDecoded.

Solution 2

Found This Link - Go down to inheritance section

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

For Decoding I did this:

 required init(from decoder: Decoder) throws {

    try super.init(from: decoder)

    let values = try decoder.container(keyedBy: CodingKeys.self)
    total = try values.decode(Int.self, forKey: .total)
  }

private enum CodingKeys: String, CodingKey
{
    case total

}

Solution 3

🚀 Swift introduced Property Wrappers in 5.1 I implemented a library called SerializedSwift that uses the power of property wrappers to Decode and Encode JSON data to objects.

One of my main goals was, to make inherited object to decode out of the box, without additonal init(from decoder: Decoder) overrides.

import SerializedSwift

class User: Serializable {

    @Serialized
    var name: String
    
    @Serialized("globalId")
    var id: String?
    
    @Serialized(alternateKey: "mobileNumber")
    var phoneNumber: String?
    
    @Serialized(default: 0)
    var score: Int
    
    required init() {}
}

// Inherited object
class PowerUser: User {
    @Serialized
    var powerName: String?

    @Serialized(default: 0)
    var credit: Int
}

It also supports custom coding keys, alternate keys, default values, custom transformation classes and many more features to be included in the future.

Available on GitHub (SerializedSwift).

Solution 4

I was able to make it work by making my base class and subclasses conform to Decodable instead of Codable. If I used Codable it would crash in odd ways, such as getting a EXC_BAD_ACCESS when accessing a field of the subclass, yet the debugger could display all the subclass values with no problem.

Additionally, passing the superDecoder to the base class in super.init() didn't work. I just passed the decoder from the subclass to the base class.

Solution 5

How about using the following way?

protocol Parent: Codable {
    var inheritedProp: Int? {get set}
}

struct Child: Parent {
    var inheritedProp: Int?
    var title: String?

    enum CodingKeys: String, CodingKey {
        case inheritedProp = "inherited_prop"
        case title = "short_title"
    }
}

Additional info on composition: http://mikebuss.com/2016/01/10/interfaces-vs-inheritance/

Share:
37,896
Kevin McQuown
Author by

Kevin McQuown

I received my BS in Computer Science from IIT back in 1984 with a digital electronics focus. I have worked in the embedded world for over 15 years both at General Dynamics on the F-16 and with Rational Software (now part of IBM). During my tenure at both companies I was involved in a lot of training and mentoring, which I love to do. I co-founded Deck5 Software back in 2009 focusing on iOS app development for Fortune 500 companies and creating some of our own products. Most recently I am thrilled to start the Windy City Lab, a digital electronics learning center here in Chicago. The fact that individuals such as ourselves can imagine a digital product and take it from inception to production with open source tools, hardware and software is absolutely fascinating and complete exhilarating to me. My hope is that the Windy City Lab can transfer my knowledge and fascination with this field to others that are interested in learning new and exciting things.

Updated on July 08, 2022

Comments

  • Kevin McQuown
    Kevin McQuown almost 2 years

    Should the use of class inheritance break the Decodability of class. For example, the following code

    class Server : Codable {
        var id : Int?
    }
    
    class Development : Server {
        var name : String?
        var userId : Int?
    }
    
    var json = "{\"id\" : 1,\"name\" : \"Large Building Development\"}"
    let jsonDecoder = JSONDecoder()
    let item = try jsonDecoder.decode(Development.self, from:json.data(using: .utf8)!) as Development
    
    print(item.id ?? "id is nil")
    print(item.name ?? "name is nil") here
    

    output is:

    1
    name is nil
    

    Now if I reverse this, name decodes but id does not.

    class Server {
        var id : Int?
    }
    
    class Development : Server, Codable {
        var name : String?
        var userId : Int?
    }
    
    var json = "{\"id\" : 1,\"name\" : \"Large Building Development\"}"
    let jsonDecoder = JSONDecoder()
    let item = try jsonDecoder.decode(Development.self, from:json.data(using: .utf8)!) as Development
    
    print(item.id ?? "id is nil")
    print(item.name ?? "name is nil")
    

    output is:

    id is nil
    Large Building Development
    

    And you can't express Codable in both classes.

  • Charlton Provatas
    Charlton Provatas almost 7 years
    was able to work around the issue for now by converting the base class to a protocol and add default implementations to the protocol extension and have the derived class conform to it
  • Harry Bloom
    Harry Bloom almost 7 years
    Same as Charlton. Was running into EXC_BAD_ACCESS errors when decoding with a base class. Had to move over to a protocol structure to get around it.
  • Joshua Nozzi
    Joshua Nozzi over 6 years
    How does this solve the problem of decoding a heterogenous array?
  • Joshua Nozzi
    Joshua Nozzi over 6 years
    Just to be clear, this wasn’t snarky criticism. I keep revisiting the problem of storing heterogenous collections to no avail. A generic solution is best, which means we can’t know the types at decoding time.
  • Tommie C.
    Tommie C. over 6 years
    In Xcode under Help > Developer Documentation, search for a great article called "Encoding and Decoding Custom Types". I think reading that will help you.
  • Jack Song
    Jack Song over 6 years
    Same trick: passing the superDecoder to the base class in super.init() didn't work. I just passed the decoder from the subclass to the base class.
  • Natanel
    Natanel over 6 years
    I'm trying to do this but I keep getting a runtime error upon encoding the data stored in an array. "Fatal error: Array<Parent> does not conform to Encodable because Parent does not conform to Encodable." Any help?
  • Nav
    Nav over 6 years
    @Natanel does your Parent conforms to Codable, if not please do so
  • Thomás Pereira
    Thomás Pereira over 6 years
    Nice blog post! Thank you for sharing.
  • Joshua Nozzi
    Joshua Nozzi over 6 years
    @TommieC. It doesn’t help because it gives no way to handle dynamic types (storing/restoring type info). You can fudge it if you don’t mind adding a dependency on ObjC class name #%-ery, but that’s not a particularly good solution.
  • mxcl
    mxcl about 6 years
    This isn't composition.
  • Doro
    Doro about 6 years
    faced same issue. is there any way to solve this without fully implementing encode/ decode methods? thanks
  • Lal Krishna
    Lal Krishna about 6 years
    Actually container.superDecoder() don't needed. super.init(from: decoder) is enough
  • Joshua Nozzi
    Joshua Nozzi about 6 years
    @LalKrishna Source? I’m following the recommendations of Apple engineers.
  • Lal Krishna
    Lal Krishna about 6 years
    i run the code swift 4.1. And I got exception while using superDecoder. And working fine with super.init(from: decoder)
  • Joshua Nozzi
    Joshua Nozzi about 6 years
    Works for me when putting my example back into a playground running Swift 4.1 (with the minor change of "simpleDecoder" to "fullDecoder", which is a mistake in the example I posted, which I've just fixed). You should probably specify the exception you're receiving.
  • ZYiOS
    ZYiOS almost 6 years
    Would you please have a look at this question? Many thanks. stackoverflow.com/questions/51211597/…
  • Tyress
    Tyress almost 6 years
    If anyone else gets the container.superDecoder() exception, see answer here: stackoverflow.com/a/47886554/1685167
  • Alexander Khitev
    Alexander Khitev almost 6 years
    @JoshuaNozzi But this is not suitable for NSManagedObject + Codable
  • Joshua Nozzi
    Joshua Nozzi almost 6 years
    @Alexander Well no ... that’s a different issue altogether to the one I was answering. :-)
  • lbarbosa
    lbarbosa almost 6 years
    @CharltonProvatas: Your approach seemed very interesting and after porting my code to use a protocol instead I ran into a swift bug that causes a crash when trying to access the type of a class implementing Codable and a custom Protocol.
  • lbarbosa
    lbarbosa almost 6 years
    Updated the ticket referenced in my previous comment with a workaround that was valid for my use case.
  • Tamás Sengel
    Tamás Sengel over 5 years
    This answer actually works better than the accepted one if you want to save a variable with a Codable subclass type to UserDefaults.
  • zhubofei
    zhubofei over 5 years
    The encode part of the answer is not correct. user2704776 got the right answer for that part.
  • WINSergey
    WINSergey over 5 years
    @JoshuaNozzi Thanks a lot for your answer. But can you go little a bit further - If I have an array of heterogeneous objects (inherited from one superclass). How can I encode/decode it? gist.github.com/w-i-n-s/cea39617a030c437af8d1ef23e0eb48d
  • Josip B.
    Josip B. about 5 years
    Once again this answer is a life saver! Big thanks @JoshuaNozzi
  • Mohammad Reza Koohkan
    Mohammad Reza Koohkan about 4 years
    This answer is not about reference types, The op question was about classes, as we have not inheritance in value types
  • Adam
    Adam almost 4 years
    I battled with the No value associated with key CodingKeys error for last 2 hours, using superDecoder saved my life, it all works now. Thanks!
  • Jens
    Jens over 3 years
    Looks good. Would this also allow to en-/decode XML? (Or are you planing to include it in the future?)
  • Dejan Skledar
    Dejan Skledar over 3 years
    @Jens definitely would be possible. The initial plan is to perfect out the API and all use cases for JSON serialization, then adding XML would not be that hard.
  • Jens
    Jens over 3 years
    Thanks! I star-ed your project on github. I went with MaxDesiatov /XMLCoder for now but it sure looks interesting!
  • Divyesh Makwana
    Divyesh Makwana over 3 years
    try super.encode(to: container.superEncoder()) added a super key while encoding
  • 6rchid
    6rchid over 3 years
    If you need to write this much init code for every subclass then you might as well copy paste all the variables you planned on inheriting, saving you more lines and time.
  • Joshua Nozzi
    Joshua Nozzi over 3 years
    @6rchid That would depend entirely on the complexity of the class, wouldn’t it? Do what works for you.
  • Dejan Skledar
    Dejan Skledar about 3 years
    @JoshuaNozzi Thank you :) I hope to upgrade the project with new features to ease developers pain on standard JSON Decodings
  • Lirik
    Lirik about 3 years
    This is the best answer here.
  • Ahmad Mahmoud Saleh
    Ahmad Mahmoud Saleh almost 3 years
    Tried this solution but It is not allowed anymore => Redundant conformance of 'XYZModel' to protocol 'Decodable'