Reputation: 6475
I've been using Codable
s in my current project with a great pleasure - everything is fine, most of the stuff I get out of the box and it's built in - perfect! Though, recently I've stumbled on a first real issue, which can't be solved automatically they way I want it.
Problem description
I have a JSON coming from the backend which is a nested thing. It looks like this
{
"id": "fef08c8d-0b16-11e8-9e00-069b808d0ecc",
"title": "Challenge_Chapter",
"topics": [
{
"id": "5145ea2c-0b17-11e8-9e00-069b808d0ecc",
"title": "Automation_Topic",
"elements": [
{
"id": "518dfb8c-0b18-11e8-9e00-069b808d0ecc",
"title": "Automated Line examle",
"type": "text_image",
"video": null,
"challenge": null,
"text_image": {
"background_url": "",
"element_render": ""
}
},
{
"id": "002a1776-0b18-11e8-9e00-069b808d0ecc",
"title": "Industry 3.0 vs. 4.0: A vision of the new manufacturing world",
"type": "video",
"video": {
"url": "https://www.youtube.com/watch?v=xxx",
"provider": "youtube"
},
"challenge": null,
"text_image": null
},
{
"id": "272fc2b4-0b18-11e8-9e00-069b808d0ecc",
"title": "Classmarker_element",
"type": "challenge",
"video": null,
"challenge": {
"url": "https://www.classmarker.com/online-test/start/",
"description": null,
"provider": "class_marker"
},
"text_image": null
}
]
}
]
}
Chapter is the root object and it contains a list of Topics and each topic contains a list of Elements. Pretty straightforward, but I get stuck with the lowest level, Elements. Each Element has an enum coming from the backend, which looks like this:
[ video, challenge, text_image ]
, but iOS app doesn't support challenges, so my ElementType enum in Swift looks like:
public enum ElementType: String, Codable {
case textImage = "text_image"
case video = "video"
}
Of, course, it throws
, because the first thing which happens is it tries to decode challenge
value for this enum and it's not there, so my whole decoding fails.
What I want
I simply want decoding process to ignore Element
s which can't be decoded. I don't need any Optional
s. I just want them not to be present in Topic's array of Elements.
My reasoning and it's drawbacks
Of course, I've made a couple of attempts to solve this problem. First one, and the simples one is just to marks ElementType
as Optional
, but with this approach later on I'll have to unwrap everything and handle this - which is rather a tedious task. My second thought was to have something like .unsupported
case in my enum, but again, later I want to use this to generate cells and I'll have to throw
or return Optional
- basically, same issues as previous idea.
My last idea, but I haven't tested it yet, is to write a custom init()
for decodable and somehow deal with it there, but I'm not sure whether it's Element
or Topic
which should be responsible for this? If I write it in Element
, I can't return nothing, I'll have to throw
, but if I put it in Topic
I'll have to append
successfully decoded elements to array. The thing is what will happen if at some point I will be fetching Elements
directly - again I won't be able to do it without throw
ing.
TL;DR
I want init(from decoder: Decoder) throws
not to throw
, but to return Optional
.
Upvotes: 1
Views: 280
Reputation: 285092
I recommend to create an umbrella protocol for all three types
protocol TypeItem {}
Edit: To conform to the requirement that only two types can be considered you have to use classes to get reference semantics
Then create classes TextImage
and Video
and a Dummy
class adopting the protocol. All instances of the Dummy class
will be removed after the decoding process.
class TextImage : TypeItem, Decodable {
let backgroundURL : String
let elementRender : String
private enum CodingKeys : String, CodingKey {
case backgroundURL = "background_url"
case elementRender = "element_render"
}
}
class Video : TypeItem, Decodable {
let url : URL
let provider : String
}
class Dummy : TypeItem {}
Use the enum to decode type
properly
enum Type : String, Decodable {
case text_image, video, challenge
}
In the struct Element
you have to implement a custom initializer which decodes the JSON to the structs depending on the type. The unwanted challange
type is decoded into a Dummy
instance. Due to the umbrella protocol you need only one property.
class Element : Decodable {
let type : Type
let id : String
let title : String
let item : TypeItem
private enum CodingKeys : String, CodingKey {
case id, title, type, video, text_image
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
title = try container.decode(String.self, forKey: .title)
type = try container.decode(Type.self, forKey: .type)
switch type {
case .text_image: item = try container.decode(TextImage.self, forKey: .text_image)
case .video: item = try container.decode(Video.self, forKey: .video)
default: item = Dummy()
}
}
}
Finally create a Root
struct for the root element and Topic
for the topics
array. In Topic
add a method to filter the Dummy
instances.
class Root : Decodable {
let id : String
let title : String
var topics : [Topic]
}
class Topic : Decodable {
let id : String
let title : String
var elements : [Element]
func filterDummy() {
elements = elements.filter{!($0.item is Dummy)}
}
}
After the decoding call filterDummy()
in each Topic
to remove the dead items.
Another downside is that you have to cast item
to the static type for example
let result = try decoder.decode(Root.self, from: data)
result.topics.forEach({$0.filterDummy()})
if let videoElement = result.topics[0].elements.first(where: {$0.type == .video}) {
let video = videoElement.item as! Video
print(video.url)
}
Upvotes: 1
Reputation: 6475
I finally found something about this in SR-5953, but I think this is a hacky one.
Anyway, for the curious ones to allow this lossy decoding you need to manually decode everything. You can write it in you init(from decoder: Decoder)
, but a better approach would be to write a new helper struct
called FailableCodableArray
. Implementation would look like:
struct FailableCodableArray<Element: Decodable>: Decodable {
// https://github.com/phynet/Lossy-array-decode-swift4
private struct DummyCodable: Codable {}
private struct FailableDecodable<Base: Decodable>: Decodable {
let base: Base?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.base = try? container.decode(Base.self)
}
}
private(set) var elements: [Element]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements = [Element]()
if let count = container.count {
elements.reserveCapacity(count)
}
while !container.isAtEnd {
guard let element = try container.decode(FailableDecodable<Element>.self).base else {
_ = try? container.decode(DummyCodable.self)
continue
}
elements.append(element)
}
self.elements = elements
}
}
And than for the actual decoding of those failable elements you jusy have to write a simple init(from decoder: Decoder)
implementation like:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
elements = try container.decode(FailableCodableArray<Element>.self, forKey: .elements).elements
}
As I've said, this solution works fine, but it feels a little hacky. It's an open bug, so you can vote it and let the Swift team see, that something like this built in would be a nice addition!
Upvotes: 3