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])") }
}