101

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.

Guilherme
  • 7,839
  • 9
  • 56
  • 99
Kevin McQuown
  • 1,277
  • 2
  • 9
  • 13
  • 1
    Interesting. Have you filed a bug with Apple? – Code Different Jun 14 '17 at 23:52
  • 1
    It's not a bug, it's literally an "undocumented feature". :-) The only reference to (half of) the solution was in the 2017 WWDC "What's New In Foundation" video, detailed in my answer below. – Joshua Nozzi Jun 17 '17 at 14:29

7 Answers7

116

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.

Andrew
  • 7,630
  • 3
  • 42
  • 51
Joshua Nozzi
  • 60,946
  • 14
  • 140
  • 135
  • 5
    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 – Charlton Provatas Aug 29 '17 at 01:03
  • 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. – Harry Bloom Sep 25 '17 at 11:56
  • 13
    Actually `container.superDecoder()` don't needed. super.init(from: decoder) is enough – Lal Krishna Jun 06 '18 at 09:39
  • @LalKrishna Source? I’m following the recommendations of Apple engineers. – Joshua Nozzi Jun 06 '18 at 18:55
  • 9
    i run the code swift 4.1. And I got exception while using superDecoder. And working fine with `super.init(from: decoder) ` – Lal Krishna Jun 07 '18 at 04:55
  • 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. – Joshua Nozzi Jun 07 '18 at 11:32
  • Would you please have a look at this question? Many thanks. https://stackoverflow.com/questions/51211597/why-codable-not-working-in-below-code – ZYiOS Jul 07 '18 at 06:55
  • If anyone else gets the container.superDecoder() exception, see answer here: https://stackoverflow.com/a/47886554/1685167 – Tyress Jul 09 '18 at 04:08
  • @JoshuaNozzi But this is not suitable for NSManagedObject + Codable – Alexander Khitev Aug 11 '18 at 21:28
  • @Alexander Well no ... that’s a different issue altogether to the one I was answering. :-) – Joshua Nozzi Aug 12 '18 at 16:20
  • @CharltonProvatas: Your approach seemed very interesting and after porting my code to use a protocol instead I ran into a [swift bug](https://bugs.swift.org/browse/SR-8158) that causes a crash when trying to access the type of a class implementing Codable and a custom Protocol. – lbarbosa Aug 28 '18 at 08:04
  • Updated the ticket referenced in my previous comment with a workaround that was valid for my use case. – lbarbosa Aug 28 '18 at 08:26
  • The encode part of the answer is not correct. user2704776 got the right answer for that part. – zhubofei Nov 16 '18 at 17:33
  • @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? https://gist.github.com/w-i-n-s/cea39617a030c437af8d1ef23e0eb48d – WINSergey Dec 02 '18 at 16:27
  • 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! – Adam Aug 06 '20 at 11:58
  • 2
    `try super.encode(to: container.superEncoder())` added a super key while encoding – Divyesh Makwana Jan 28 '21 at 04:34
  • 1
    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. – 6rchid Mar 24 '21 at 23:31
  • @6rchid That would depend entirely on the complexity of the class, wouldn’t it? Do what works for you. – Joshua Nozzi Mar 26 '21 at 00:17
32

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

}
devjme
  • 684
  • 6
  • 12
12

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).

Dejan Skledar
  • 11,280
  • 7
  • 44
  • 70
  • Looks good. Would this also allow to en-/decode XML? (Or are you planing to include it in the future?) – Jens Nov 02 '20 at 16:35
  • 1
    @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. – Dejan Skledar Nov 04 '20 at 12:30
  • Thanks! I star-ed your project on github. I went with [MaxDesiatov /XMLCoder](https://github.com/MaxDesiatov/XMLCoder) for now but it sure looks interesting! – Jens Nov 04 '20 at 13:00
  • @JoshuaNozzi Thank you :) I hope to upgrade the project with new features to ease developers pain on standard JSON Decodings – Dejan Skledar Apr 08 '21 at 13:16
  • 1
    this looks more like what I expect – Eman Jul 21 '22 at 15:15
  • 1
    This saved me soooo much grunt work. Thanks man! – Renegade Oct 14 '22 at 18:47
5

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.

Milad Faridnia
  • 9,113
  • 13
  • 65
  • 78
ShackBurger
  • 61
  • 1
  • 2
  • 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. – Jack Song Dec 30 '17 at 11:24
  • faced same issue. is there any way to solve this without fully implementing encode/ decode methods? thanks – Doro May 28 '18 at 15:14
  • 1
    Tried this solution but It is not allowed anymore => `Redundant conformance of 'XYZModel' to protocol 'Decodable'` – Ahmad Mahmoud Saleh Jul 27 '21 at 19:48
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/

Nav
  • 10,304
  • 20
  • 56
  • 83
  • 4
    How does this solve the problem of decoding a heterogenous array? – Joshua Nozzi Oct 19 '17 at 15:25
  • 2
    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. – Joshua Nozzi Nov 07 '17 at 16:55
  • In Xcode under Help > Developer Documentation, search for a great article called "Encoding and Decoding Custom Types". I think reading that will help you. – Tommie C. Nov 13 '17 at 13:30
  • I'm trying to do this but I keep getting a runtime error upon encoding the data stored in an array. "Fatal error: Array does not conform to Encodable because Parent does not conform to Encodable." Any help? – Natanel Jan 09 '18 at 17:23
  • @Natanel does your Parent conforms to Codable, if not please do so – Nav Jan 23 '18 at 09:06
  • @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. – Joshua Nozzi Mar 26 '18 at 14:50
  • 4
    This isn't composition. – mxcl May 16 '18 at 18:28
  • This answer is not about reference types, The op question was about classes, as we have not inheritance in value types – Mohammad Reza Koohkan Jun 26 '20 at 19:01
5

Here is a library TypePreservingCodingAdapter to do just that (can be installed with Cocoapods or SwiftPackageManager).

The code below compiles and works just fine with Swift 4.2. Unfortunately for every subclass you'll need to implement encoding and decoding of properties on your own.

import TypePreservingCodingAdapter
import Foundation

// redeclared your types with initializers
class Server: Codable {
    var id: Int?

    init(id: Int?) {
        self.id = id
    }
}

class Development: Server {
    var name: String?
    var userId: Int?

    private enum CodingKeys: String, CodingKey {
        case name
        case userId
    }

    init(id: Int?, name: String?, userId: Int?) {
        self.name = name
        self.userId = userId
        super.init(id: id)
    }

    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        let container = try decoder.container(keyedBy: CodingKeys.self)

        name = try container.decodeIfPresent(String.self, forKey: .name)
        userId = try container.decodeIfPresent(Int.self, forKey: .userId)
    }

    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(userId, forKey: .userId)
    }

}

// create and adapter
let adapter = TypePreservingCodingAdapter()
let encoder = JSONEncoder()
let decoder = JSONDecoder()

// inject it into encoder and decoder
encoder.userInfo[.typePreservingAdapter] = adapter
decoder.userInfo[.typePreservingAdapter] = adapter

// register your types with adapter
adapter.register(type: Server.self).register(type: Development.self)


let server = Server(id: 1)
let development = Development(id: 2, name: "dev", userId: 42)

let servers: [Server] = [server, development]

// wrap specific object with Wrap helper object
let data = try! encoder.encode(servers.map { Wrap(wrapped: $0) })

// decode object back and unwrap them force casting to a common ancestor type
let decodedServers = try! decoder.decode([Wrap].self, from: data).map { $0.wrapped as! Server }

// check that decoded object are of correct types
print(decodedServers.first is Server)     // prints true
print(decodedServers.last is Development) // prints true
Igor Muzyka
  • 410
  • 7
  • 13
0

Swift 5

The compiler synthesises decodable code only for a type that directly adopts Codable protocol so that you observe decoding for a single of your type in inheritance.

But you can try next generic approach with KeyValueCoding package (https://github.com/ikhvorost/KeyValueCoding) and this package provides access to all properties metadata and allows to get/set any property for pure swift types dynamically. The idea is to make a base Coding class which adopts KeyValueCoding and implements decoding of all available properties in init(from: Decoder):

class Coding: KeyValueCoding, Decodable {
    
    typealias DecodeFunc = (KeyedDecodingContainer<_CodingKey>, _CodingKey) throws -> Any?
    
    struct _CodingKey: CodingKey {
      let stringValue: String
      let intValue: Int?

      init(stringValue: String) {
        self.stringValue = stringValue
        self.intValue = Int(stringValue)
      }

      init(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
      }
    }
    
    static func decodeType<T: Decodable>(_: T.Type) -> (type: T.Type, f: DecodeFunc)  {
        (T.self,  { try $0.decode(T.self, forKey: $1) })
    }
    
    static var decodeTypes: [(Any.Type, DecodeFunc)] = [
        decodeType(Int.self),
        decodeType(Int?.self),
        decodeType(String.self),
        decodeType(String?.self),
        // Other types to support...
    ]
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: _CodingKey.self)
        try container.allKeys.forEach { codingKey in
            let key = codingKey.stringValue
            guard let property = (properties.first { $0.name == key }),
                let item = (Self.decodeTypes.first { property.type == $0.0 })
            else {
                return
            }
            var this = self
            this[key] = try item.1(container, codingKey)
        }
    }
}

It is important to provide all supported types to decode in decodeTypes variable.

How to use:

class Server: Coding {
    var id: Int?
}

class Development : Server {
    var name: String = ""
}

class User: Development {
    var userId: Int = 0
}

func decode() {
    let json = "{\"id\": 1, \"name\": \"Large Building Development\", \"userId\": 123}"
    do {
        let user = try JSONDecoder().decode(User.self, from:json.data(using: .utf8)!)
        print(user.id, user.name, user.userId) // Optional(1) Large Building Development 123
    }
    catch {
        print(error.localizedDescription)
    }
}
iUrii
  • 11,742
  • 1
  • 33
  • 48