3
votes

I am using the Codable protocols to decode JSON from a Web API. My Swift data model for this API includes both class inheritance (subclasses), and composition (objects as properties of other objects). In the JSON, the same property name may represent a complete object, or a single string denoting the id of that object in a database.

To my knowledge, the only pattern for handling this sort of JSON using Codable is to do the decoding "manually" within the object's initializer init(from decoder: Decoder), and to first try to decode the entire object. Should that fail (by throwing an error which must be caught), then to retry decoding the same property as a String.

This works well, so long as the object containing the varient property is not a subclass of another Decodable class. When that is the case, decoding the properties of the base class will throw the error DecodingError.typeMismatch on the call to function decoder.container(keyedBy:).

See my sample code below.

Is this a known bug? And/or am I missing an alternate method of decoding in this situation?

Incidentally, the same error will be thrown within a single function, if decoder.container(keyedBy:) is called after a DecodingError.typeMismatch error is thrown, even if that error was caught.

import Foundation

// A `Base` class
class Base: Codable {
    var baseProperty: String? = "baseProperty"
    init() {}

    private enum CodingKeys: String, CodingKey { case baseProperty }

    required init(from decoder: Decoder) throws {
//===>> The next line will throw DecodingError.typeMismatch
        let container = try decoder.container(keyedBy: CodingKeys.self)
        baseProperty = try container.decode(String.self, forKey: .baseProperty)
    }
}

// A Subclass of `Base`
class Sub: Base {
    // An `Other` class which is a property of the `Sub` class
    class Other: Codable { var id: String? = "otherID" }
    var subProperty: Other? = nil
    override init() { super.init() }

    private enum CodingKeys: String, CodingKey { case subProperty }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do { subProperty = try container.decode(Other.self,
                                                forKey: .subProperty)
        }
        catch { // We didn't find a whole `Other` object in the JSON; look for a `String` instead
            let s = try container.decode(String.self, forKey: .subProperty)
            subProperty = Other()
            subProperty?.id = s
        }
        try super.init(from: decoder)
    }
}

// Some sample JSON data:
let json = """
{"baseProperty" : "baseProperty",
 "subProperty" : "someIDString"}
""".data(using: .utf8)!

// MAIN program -----------------------------------------------------
// Decode the JSON to produce a new Sub class instance
do {
    _ = try JSONDecoder().decode(Sub.self, from: json)
}
catch DecodingError.typeMismatch( _, let context) {
    print("DecodingError.typeMismatch: \(context.debugDescription)")
    print("DecodingError.Context: codingPath:")
    for i in 0..<context.codingPath.count { print("  [\(i)] = \(context.codingPath[i])") }
}
1

1 Answers

3
votes

This is a known bug, which has been fixed in this pull request (and will make it into Swift 4.1).

The issue is basically that when the decoding of Other fails, the decoder will forget to pop off its container from its internal stack, therefore meaning that any future decodes start in the nested container for .subProperty; hence why you get a type mismatch error on attempting to decode an object from there (as there's only a string!).

Until fixed, one workaround is rather than using decode(_:forKey:); get a super decoder, and then attempt to decode an Other from that.

So replace this:

subProperty = try container.decode(Other.self, forKey: .subProperty)

with this:

let subPropertyDecoder = try container.superDecoder(forKey: .subProperty)
subProperty = try Other(from: subPropertyDecoder)

This works because now we've got a completely new decoder instance, we can't corrupt the stack of the main decoder.