Adding a new property to a model: converting existing JSON - json

say I had this model:
struct MyModel: Codable {
var name: String
}
By already using this property, I persisted JSON files like this:
{"name":"Foo"}
Now, I want to add a new property uuid, but want to make sure to have a valid migration path to locally persisted existing data. So, I rewrote my struct to this:
struct MyModel {
let uuid: UUID
var name: String
}
extension MyModel: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.uuid = try container.decode(UUID.self, forKey: .uuid)
} catch {
self.uuid = UUID()
}
self.name = try container.decode(String.self, forKey: .name)
}
enum CodingKeys: String, CodingKey {
case uuid
case name
}
}
My thought process here is to catch the error when not finding the property in the JSON by instantiating it manually when needed.
This leads to the error The data couldn’t be read because it is missing..
I might have to add that to be able to load the locally persisted data, I have built the function ATPersistLocally.loadAll(of:) in a personal toolbox package (that I can add to my projects with SPM), that looks like this:
public func loadAll<T: Codable & Identifiable>(of type: T.Type) -> [T] {
var result = [T]()
let pathFolder = docPath.appendingPathComponent(String(describing: type))
if fileManager.fileExists(atPath: pathFolder.path) {
var urls = [URL]()
do {
urls = try fileManager.contentsOfDirectory(at: pathFolder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
} catch {
ATLogger.shared.logToConsole(message: "Could not load content of directory: \(error.localizedDescription)", type: .error)
}
for url in urls {
if let data = fileManager.contents(atPath: url.path) {
do {
let instance = try decoder.decode(T.self, from: data)
result.append(instance)
} catch {
ATLogger.shared.logToConsole(message: "Could not decode data: \(error.localizedDescription)", type: .error)
}
}
}
}
return result
}
Any hints on how to resolve this?

Simple solution: Omit the CodingKey uuid to decode only name (and you can omit the init method, too).
struct MyModel {
let uuid = UUID()
var name: String
}
extension MyModel: Codable {
enum CodingKeys: String, CodingKey { case name }
}
Important note:
Never print error.localizedDescription in a Decoding catch block, print always only the error instance to get the actual descriptive error message.

Use decodeIfPresentinstead in init(from decoder: Decoder) together with a local variable
if let uuid = try container.decodeIfPresent(UUID.self, forKey: .uuid) {
self.uuid = uuid
} else {
self.uuid = UUID()
}

Related

Swift Codable: subclass JSONDecoder for custom behavior

I'm having an inconsistent API that might return either a String or an Number as a part of the JSON response.
The dates also could be represented the same way as either a String or a Number, but are always an UNIX timestamp (i.e. timeIntervalSince1970).
To fix the issue with the dates, I simply used a custom JSONDecoder.DateDecodingStrategy:
decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.custom({ decoder in
let container = try decoder.singleValueContainer()
if let doubleValue = try? container.decode(Double.self) {
return Date(timeIntervalSince1970: doubleValue)
} else if let stringValue = try? container.decode(String.self),
let doubleValue = Double(stringValue) {
return Date(timeIntervalSince1970: doubleValue)
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Unable to decode value of type `Date`")
})
However, no such customization is available for the Int or Double types which I'd like to apply it for.
So, I have to resort to writing Codable initializers for each of the model types that I'm using.
The alternative approach I'm looking for is to subclass the JSONDecoder and override the decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable method.
In that method I'd like to "inspect" the type T that I'm trying to decode to and then, if the base implementation (super) fails, try to decode the value first to String and then to the T (the target type).
So far, my initial prototype looks like this:
final class CustomDecoder: JSONDecoder {
override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
do {
return try super.decode(type, from: data)
} catch {
if type is Int.Type {
print("Trying to decode as a String")
if let decoded = try? super.decode(String.self, from: data),
let converted = Int(decoded) {
return converted as! T
}
}
throw error
}
}
}
However, I found out that the "Trying to decode as a String" message is never printed for some reason, even though the control reaches the catch stage.
I'm happy to have that custom path only for Int and Double types, since the T is Codable and that doesn't guarantee ability to initialize a value with the String, however, I of course welcome a more generalized approach.
Here's the sample Playground code that I came up with to test my prototype. It can be copy-pasted directly into the Playground and works just fine.
My goal is to have both jsonsample1 and jsonsample2 to produce the same result.
import UIKit
final class CustomDecoder: JSONDecoder {
override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
do {
return try super.decode(type, from: data)
} catch {
if type is Int.Type {
print("Trying to decode as a String")
if let decoded = try? super.decode(String.self, from: data),
let converted = Int(decoded) {
return converted as! T
}
}
throw error
}
}
}
let jsonSample1 =
"""
{
"name": "Paul",
"age": "38"
}
"""
let jsonSample2 =
"""
{
"name": "Paul",
"age": 38
}
"""
let data1 = jsonSample1.data(using: .utf8)!
let data2 = jsonSample2.data(using: .utf8)!
struct Person: Codable {
let name: String?
let age: Int?
}
let decoder = CustomDecoder()
let person1 = try? decoder.decode(Person.self, from: data1)
let person2 = try? decoder.decode(Person.self, from: data2)
print(person1 as Any)
print(person2 as Any)
What could be the reason for my CustomDecoder not working?
The primary reason that your decoder doesn't do what you expect is that you're not overriding the method that you want to be: JSONDecoder.decode<T>(_:from:) is the top-level method that is called when you call
try JSONDecoder().decode(Person.self, from: data)
but this is not the method that is called internally during decoding. Given the JSON you show as an example, if we write a Person struct as
struct Person: Decodable {
let name: String
let age: Int
}
then the compiler will write an init(from:) method which looks like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
}
Note that when we decode age, we are not calling a method on the decoder directly, but on a KeyedCodingContainer that we get from the decoder — specifically, the Int.Type overload of KeyedDecodingContainer.decode(_:forKey:).
In order to hook into the methods that are called during decode at the middle levels of a Decoder, you'd need to hook into its actual container methods, which is very difficult — all of JSONDecoder's containers and internals are private. In order to do this by subclassing JSONDecoder, you'd end up needing to pretty much reimplement the whole thing from scratch, which is significantly more complicated than what you're trying to do.
As suggested in a comment, you're likely better off either:
Writing Person.init(from:) manually by trying to decode both Int.self and String.self for the .age property and keeping whichever one succeeds, or
If you need to reuse this solution across many types, you can write a wrapper type to use as a property:
struct StringOrNumber: Decodable {
let number: Double
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
number = try container.decode(Double.self)
} catch (DecodingError.typeMismatch) {
let string = try container.decode(String.self)
if let n = Double(string) {
number = n
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value wasn't a number or a string...")
}
}
}
}
struct Person: Decodable {
let name: String
let age: StringOrNumber
}
You can also write StringOrNumber as an enum which can hold either case string(String) or case number(Double) if knowing which type of value was in the payload was important:
enum StringOrNumber: Decodable {
case number(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .number(container.decode(Double.self))
} catch (DecodingError.typeMismatch) {
let string = try container.decode(String.self)
if let n = Double(string) {
self = .string(string)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value wasn't a number or a string...")
}
}
}
}
Though this isn't as relevant if you always need Double/Int access to the data, since you'd need to re-convert at the use site every time (and you call this out in a comment)

Swift conditional conformance with two cases

The short question is how can I make a type conditionally conform to a protocol with two conditions when either one of them is met?
I have a generic type NetworkResponse<Data>. It represents server response. Here is how it is defined:
enum NetworkResponse<Data> {
case success(Data)
case error(ServerError)
}
I want to make NetworkResponse to conform Decodable. Here is my server response format:
{
"data": {
"someKey": "someValue",
"anotherKey": 15
},
"meta": {
"returnCode": 0,
"returnMessage": "operation is successful"
}
}
The data part depends on what request is made. The meta part represents some meta data about response. Like whether it is success or if not what is the error.
So here is how I implemented Decodable:
extension NetworkResponse: Decodable where Data: Decodable {
enum CodingKeys: CodingKey {
case meta
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let metaValue = try container.decode(ServerError.self, forKey: .meta)
if metaValue.code != 0 {
self = .error(metaValue)
} else {
self = .success(try container.decode(Data.self, forKey: .data))
}
}
}
So far so good. But here is my problem. For some apis which don't need to return any data the data part in response is omitted. In this case my response would look like this:
{
"meta": {
"returnCode": 0,
"returnMessage": "operation is successful"
}
}
In this case I want to decode the response json as NetworkResponse<Void>. But since Void can not conform to Decodable (since it is non nominal type) compiler gives error.
To overcome this I tried to create more specialized extension of Decodable where Data is Void like this:
extension NetworkResponse: Decodable where Data == Void {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let metaValue = try container.decode(AppErrors.Server.self, forKey: CodingKeys.meta)
if metaValue.code != 0 {
self = .error(metaValue)
} else {
self = .success(())
}
}
}
But still compiler complines like: Conflicting conformance of 'NetworkResponse<Data>' to protocol 'Decodable'; there cannot be more than one conformance, even with different conditional bounds.
So how can I create seperate init(from:) function that is used for when Data is Void?
I strongly urge you to change the generic parameter's type name away from Data since it's too easily confused with Foundation.Data which is widely used throughout Swift.
On to the problem itself, you can make an empty struct to represent "void" and add a new case to your NetworkResponse:
struct EmptyData: Decodable {}
enum NetworkResponse<T> {
case success(T)
case successWithEmptyData
case error(ServerError)
}
extension NetworkResponse: Decodable where T: Decodable {
private enum CodingKeys: CodingKey {
case meta
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let metaValue = try container.decode(ServerError.self, forKey: .meta)
if metaValue.code != 0 {
self = .error(metaValue)
} else if T.self == EmptyData.self {
self = .successWithEmptyData
} else {
self = .success(try container.decode(T.self, forKey: .data))
}
}
}
let response = try! JSONDecoder().decode(NetworkResponse<EmptyData>.self, from: jsonData)
Alternatively, you can make your .success case contains a T? when you expect no data in return and decode accordingly.

Swift 4 JSON Parsing Int - what am I doing wrong?

Swift 4 JSON Parsing Int issue. All the examples I've seen Int is encoded / decoded out of the box. Anyone see what I am doing wrong?
Thanks!
JSON
let patientBundleEntry = """
{
"resourceType": "Bundle",
"id": "patientListBundle",
"type": "SearchSet",
"total": 123
}
"""
Classes
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int? // this is printing -> Optional(105553116787496) instead of 123
}
class Resource:Codable {
var resourceType:ResourceType? // this is printing fine
}
Test - my assert for total being 123 fails and the optional is a long number. Any ideas why? Is my encoding wrong using .utf8??
func testModelBundle(){
let jsonDataEncoded:Data? = patientBundleEntry.data(using: .utf8)!
guard let responseData = jsonDataEncoded else {
print("Error: did not receive data")
}
do {
let bundleDecoded = try JSONDecoder().decode(BundleFHIR.self, from: responseData)
print("bundleDecoded.resourceType resource type \(bundleDecoded.resourceType )") //THIS is right
print("bundleDecoded.resourceType total \(bundleDecoded.total )") THIS is wrong
assert(bundleDecoded.total == 123, "something is wrong") // ***** <- this assert fails and it prints Optional(105553116787496)
} catch {
print("error trying to convert data to JSON")
}
}
At first you have to decode and then parse the JSON data.
Follow the below code :
struct Patient: Codable {
var resourceType: String
var id: String
var type: String
var total: Int
}
let json = patientBundleEntry.data(using: .utf8)!
let decoder = JSONDecoder()
let patient = try! decoder.decode(Patient.self, from: json)
print(patient.total) // Prints - 123
Ok there were a lot of issues with my code. Mainly that I didn't have codingKeys as private and therefore tried to rename it because the inheritance tree could not discern between the two. This caused me to not be implementing the true protocol. Not sure why it was half working... but here is my final code and it works great!
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int?
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
total = try values.decodeIfPresent(Int.self, forKey: .total)
entry = try values.decodeIfPresent([BundleEntry].self, forKey: .entry)
}
private enum CodingKeys: String, CodingKey
{
case total
case entry
}
}
class Resource:Codable {
var resourceType:ResourceType?
var id:String?
init(){
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
resourceType = try values.decode(ResourceType.self, forKey: .resourceType)
id = try values.decode(String.self, forKey: .id)
}
private enum CodingKeys: String, CodingKey
{
case resourceType
case id
}
}

Swift 4 Decodable multiple containers

I'm trying to understand how could I parse this multiple container JSON to an object. I've tried this approach (Mark answer), but he explain how to solve it using one-level container. For some reason I can't mimic the behaviour for multiple containers.
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql // The top level "user" key
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let post = try values.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}
I'm getting:
Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot get KeyedDecodingContainer<PostKeys> -- no value found for key \"shortcode_media\"", underlyingError: nil))
Any help will be much appreciated, thank you!
As vadian notes, you haven't matched the JSON structure. There is no shortcode_media key at the top level like you've encoded in CodingKeys.
In order to decode this with a custom decoder, you will need to walk through each level and deal with it.
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql
}
enum GraphQLKeys: String, CodingKey {
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
// unload the top level
let container = try decoder.container(keyedBy: CodingKeys.self)
// Unload the graphql key
let graphql = try container.nestedContainer(keyedBy: GraphQLKeys.self, forKey: .graphql)
// unload the shortcode_media key
let post = try graphql.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
// Finally, unload the actual object
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}
Please read the JSON.
Any opening { is quasi a separator. The indentation of the JSON indicates also the hierarchy.
For clarity I removed all coding keys and left the variable names – which should be camelCased – unchanged.
struct Root : Decodable {
let graphql : Graph
// to access the `Media` object declare a lazy instantiated property
lazy var media : Media = {
return graphql.shortcode_media
}()
}
struct Graph : Decodable {
let shortcode_media : Media
}
struct Media : Decodable {
let id: String
let shortcode : String
}
let jsonString = """
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
"""
do {
let data = Data(jsonString.utf8)
var result = try decoder.decode(Root.self, from: data)
print(result.media)
} catch {
print("error: ", error)
}
Writing a custom initializer with nestedContainer is more effort than creating the actual hierarchy.
Please paste the entire code in a Playground and check it out.

How can I implement polymorphic decoding of JSON data in Swift 4?

I am attempting to render a view from data returned from an API endpoint. My JSON looks (roughly) like this:
{
"sections": [
{
"title": "Featured",
"section_layout_type": "featured_panels",
"section_items": [
{
"item_type": "foo",
"id": 3,
"title": "Bisbee1",
"audio_url": "http://example.com/foo1.mp3",
"feature_image_url" : "http://example.com/feature1.jpg"
},
{
"item_type": "bar",
"id": 4,
"title": "Mortar8",
"video_url": "http://example.com/video.mp4",
"director" : "John Smith",
"feature_image_url" : "http://example.com/feature2.jpg"
}
]
}
]
}
I have an object that represents how to layout a view in my UI. It looks like this:
public struct ViewLayoutSection : Codable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
}
ViewLayoutSectionItemable is a protocol that includes, among other things, a title and a URL to an image to use in the layout.
However, the sectionItems array is actually made up of different types. What I'd like to do is instantiate each section item as an instance of its own class.
How do I setup the init(from decoder: Decoder) method for the ViewLayoutSection to let me iterate over the items in that JSON array and create an instance of the proper class in each case?
Polymorphic design is a good thing: many design patterns exhibit polymorphism to make the overall system more flexible and extensible.
Unfortunately, Codable doesn't have "built in" support for polymorphism, at least not yet.... there's also discussion about whether this is actually a feature or a bug.
Fortunately, you can pretty easily create polymorphic objects using an enum as an intermediate "wrapper."
First, I'd recommend declaring itemType as a static property, instead of an instance property, to make switching on it easier later. Thereby, your protocol and polymorphic types would look like this:
import Foundation
public protocol ViewLayoutSectionItemable: Decodable {
static var itemType: String { get }
var id: Int { get }
var title: String { get set }
var imageURL: URL { get set }
}
public struct Foo: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "foo" }
public let id: Int
public var title: String
public var imageURL: URL
// Foo Properties
public var audioURL: URL
}
public struct Bar: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "bar" }
public let id: Int
public var title: String
public var imageURL: URL
// Bar Properties
public var director: String
public var videoURL: URL
}
Next, create an enum for the "wrapper":
public enum ItemableWrapper: Decodable {
// 1. Keys
fileprivate enum Keys: String, CodingKey {
case itemType = "item_type"
case sections
case sectionItems = "section_items"
}
// 2. Cases
case foo(Foo)
case bar(Bar)
// 3. Computed Properties
public var item: ViewLayoutSectionItemable {
switch self {
case .foo(let item): return item
case .bar(let item): return item
}
}
// 4. Static Methods
public static func items(from decoder: Decoder) -> [ViewLayoutSectionItemable] {
guard let container = try? decoder.container(keyedBy: Keys.self),
var sectionItems = try? container.nestedUnkeyedContainer(forKey: .sectionItems) else {
return []
}
var items: [ViewLayoutSectionItemable] = []
while !sectionItems.isAtEnd {
guard let wrapper = try? sectionItems.decode(ItemableWrapper.self) else { continue }
items.append(wrapper.item)
}
return items
}
// 5. Decodable
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
let itemType = try container.decode(String.self, forKey: Keys.itemType)
switch itemType {
case Foo.itemType: self = .foo(try Foo(from: decoder))
case Bar.itemType: self = .bar(try Bar(from: decoder))
default:
throw DecodingError.dataCorruptedError(forKey: .itemType,
in: container,
debugDescription: "Unhandled item type: \(itemType)")
}
}
}
Here's what the above does:
You declare Keys that are relevant to the response's structure. In your given API, you're interested in sections and sectionItems. You also need to know which key represents the type, which you declare here as itemType.
You then explicitly list every possible case: this violates the Open Closed Principle, but this is "okay" to do as it's acting as a "factory" for creating items....
Essentially, you'll only have this ONCE throughout your entire app, just right here.
You declare a computed property for item: this way, you can unwrap the underlying ViewLayoutSectionItemable without needing to care about the actual case.
This is the heart of the "wrapper" factory: you declare items(from:) as a static method that's capable of returning [ViewLayoutSectionItemable], which is exactly what you want to do: pass in a Decoder and get back an array containing polymorphic types! This is the method you'll actually use instead of decoding Foo, Bar or any other polymorphic arrays of these types directly.
Lastly, you must make ItemableWrapper implement the Decodable method. The trick here is that ItemWrapper always decodes an ItemWrapper: thereby, this works how Decodable is expecting.
As it's an enum, however, it's allowed to have associated types, which is exactly what you do for each case. Hence, you can indirectly create polymorphic types!
Since you've done all the heavy lifting within ItemWrapper, it's very easy to now go from a Decoder to an `[ViewLayoutSectionItemable], which you'd do simply like this:
let decoder = ... // however you created it
let items = ItemableWrapper.items(from: decoder)
A simpler version of #CodeDifferent's response, which addresses #JRG-Developer's comment. There is no need to rethink your JSON API; this is a common scenario. For each new ViewLayoutSectionItem you create, you only need to add one case and one line of code to the PartiallyDecodedItem.ItemKind enum and PartiallyDecodedItem.init(from:) method respectively.
This is not only the least amount of code compared to the accepted answer, it is more performant. In #CodeDifferent's option, you are required to initialize 2 arrays with 2 different representations of the data to get your array of ViewLayoutSectionItems. In this option, you still need to initialize 2 arrays, but get to only have one representation of the data by taking advantage of copy-on-write semantics.
Also note that it is not necessary to include ItemType in the protocol or the adopting structs (it doesn't make sense to include a string describing what type a type is in a statically typed language).
protocol ViewLayoutSectionItem {
var id: Int { get }
var title: String { get }
var imageURL: URL { get }
}
struct Foo: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let audioURL: URL
}
struct Bar: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let videoURL: URL
let director: String
}
private struct PartiallyDecodedItem: Decodable {
enum ItemKind: String, Decodable {
case foo, bar
}
let kind: Kind
let item: ViewLayoutSectionItem
private enum DecodingKeys: String, CodingKey {
case kind = "itemType"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DecodingKeys.self)
self.kind = try container.decode(Kind.self, forKey: .kind)
self.item = try {
switch kind {
case .foo: return try Foo(from: decoder)
case .number: return try Bar(from: decoder)
}()
}
}
struct ViewLayoutSection: Decodable {
let title: String
let sectionLayoutType: String
let sectionItems: [ViewLayoutSectionItem]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .title)
self.sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
self.sectionItems = try container.decode([PartiallyDecodedItem].self, forKey: .sectionItems)
.map { $0.item }
}
}
To handle the snake case -> camel case conversion, rather than manually type out all of the keys, you can simply set a property on JSONDecoder
struct Sections: Decodable {
let sections: [ViewLayoutSection]
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
.sections
I recommend you to be judicious on the use of Codable. If you only want to decode a type from JSON and not encode it, conforming it to Decodable alone is enough. And since you have already discovered that you need to decode it manually (via a custom implementation of init(from decoder: Decoder)), the question becomes: what is the least painful way to do it?
First, the data model. Note that ViewLayoutSectionItemable and its adopters do not conform to Decodable:
enum ItemType: String, Decodable {
case foo
case bar
}
protocol ViewLayoutSectionItemable {
var id: Int { get }
var itemType: ItemType { get }
var title: String { get set }
var imageURL: URL { get set }
}
struct Foo: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Foo
var audioURL: URL
}
struct Bar: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Bar
var videoURL: URL
var director: String
}
Next, here's how we will decode the JSON:
struct Sections: Decodable {
var sections: [ViewLayoutSection]
}
struct ViewLayoutSection: Decodable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
// This struct use snake_case to match the JSON so we don't have to provide a custom
// CodingKeys enum. And since it's private, outside code will never see it
private struct GenericItem: Decodable {
let id: Int
let item_type: ItemType
var title: String
var feature_image_url: URL
// Custom properties of all possible types. Note that they are all optionals
var audio_url: URL?
var video_url: URL?
var director: String?
}
private enum CodingKeys: String, CodingKey {
case title
case sectionLayoutType = "section_layout_type"
case sectionItems = "section_items"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
sectionItems = try container.decode([GenericItem].self, forKey: .sectionItems).map { item in
switch item.item_type {
case .foo:
// It's OK to force unwrap here because we already
// know what type the item object is
return Foo(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, audioURL: item.audio_url!)
case .bar:
return Bar(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, videoURL: item.video_url!, director: item.director!)
}
}
}
Usage:
let sections = try JSONDecoder().decode(Sections.self, from: json).sections
I have written a blog post about this exact problem.
In summary. I suggest defining an extension on Decoder
extension Decoder {
func decode<ExpectedType>(_ expectedType: ExpectedType.Type) throws -> ExpectedType {
let container = try self.container(keyedBy: PolymorphicMetaContainerKeys.self)
let typeID = try container.decode(String.self, forKey: .itemType)
guard let types = self.userInfo[.polymorphicTypes] as? [Polymorphic.Type] else {
throw PolymorphicCodableError.missingPolymorphicTypes
}
let matchingType = types.first { type in
type.id == typeID
}
guard let matchingType = matchingType else {
throw PolymorphicCodableError.unableToFindPolymorphicType(typeID)
}
let decoded = try matchingType.init(from: self)
guard let decoded = decoded as? ExpectedType else {
throw PolymorphicCodableError.unableToCast(
decoded: decoded,
into: String(describing: ExpectedType.self)
)
}
return decoded
}
}
Then adding the possible polymorphic types to the Decoder instance:
var decoder = JSONDecoder()
decoder.userInfo[.polymorphicTypes] = [
Snake.self,
Dog.self
]
If you have nested polymeric values you can write a property wrapper to that calls this decode method so that you do not need to define custom init(from:).
Here's a small utility package that resolve this exact problem.
It was built around a configuration type that has variants for the decodable type defines the type information discriminator.
enum DrinkFamily: String, ClassFamily {
case drink = "drink"
case beer = "beer"
static var discriminator: Discriminator = .type
typealias BaseType = Drink
func getType() -> Drink.Type {
switch self {
case .beer:
return Beer.self
case .drink:
return Drink.self
}
}
}
Later in your collection overload the init method to use our KeyedDecodingContainer extension.
class Bar: Decodable {
let drinks: [Drink]
private enum CodingKeys: String, CodingKey {
case drinks
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
drinks = try container.decodeHeterogeneousArray(OfFamily: DrinkFamily.self, forKey: .drinks)
}
}