Ignoring not supported Decodables - json

I've been using Codables 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 Elements which can't be decoded. I don't need any Optionals. 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 throwing.
TL;DR
I want init(from decoder: Decoder) throws not to throw, but to return Optional.

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!

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

Related

How to Parse Nested part of a JSON based on a condition in Swift

Other suggested solutions handle type of structure they are part of, While I am not able to figure out how to parse nested part of that same structure based on the value within outer structure.
I have a JSON response whose structure change based on one of the values within outer part of JSON.
For example:
{
"reports": [
{
"reportType": "advance",
"reportData": {
"value1": "true"
}
},
{
"reportType": "simple",
"reportData": {
"value3": "false",
"value": "sample"
}
}
]
}
Using Codable with string as Type for 'report' key fails to parse this json.
I want this report value to be either parsed later and store it as it is or atleast parse it based on the reportType value as this have different structure for each value of reportType.
I have written code based on the the suggested solutions.
enum ReportTypes: String {
case simple, advance
}
struct Reports: Codable {
let reportArray = [Report]
}
struct Report: Decodable {
let reportType: String
let reportData: ReportTypes
enum CodingKeys: String, CodingKey {
case reportType, reportData
}
init(from decoder: Decoder) {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.reportType = try container.decode(String.self, forKey: .reportType)
switch ReportTypes(rawValue: self.reportType) {
case .simple:
???
case .advance:
???
}
}
}
Please look at the switch cases and i'm not sure what to do. I need a solution similar to do this.
Workaround:
The workaround is that to mode that reportType inside the report {} structure and then follow this question How can i parse an Json array of a list of different object using Codable?
New Structure
{
"reports": [
{
"reportType": "advance",
"reportData": {
"reportType": "advance",
"value1": "true"
}
},
{
"reportType": "simple",
"reportData": {
"reportType": "simple",
"value3": "false",
"value": "sample"
}
}
]
}
So it worked out for me this way.
But if changing the structure is not what you can afford then this will not work.
Other possible solution I see and later Question: How to Access value of Codable Parent struct in a nested Codable struct is storing reportType in variable currentReportType from init(from decoder: Decoder) and then write another decoder for struct reportData that will handle decoding based on the value stored in var currentReportType. Write it by following the first link shared.
A reasonable way to decode this JSON is an enum with associated values because the type is known.
Create two structs for the objects representing the data in reportData
struct AdvancedReport: Decodable {
let value1: String
}
struct SimpleReport: Decodable {
let value3, value: String
}
and adopt Decodable in ReportType
enum ReportType: String, Decodable {
case simple, advance
}
The two main structs are the struct Response which is the root object (I renamed it because there are too many occurrences of the term Report) and the mentioned enum with associated values. The key reportType in the child dictionary is not being decoded because it's redundant.
struct Response: Decodable {
let reports : [Report]
}
enum Report: Decodable {
case simple(SimpleReport)
case advance(AdvancedReport)
private enum CodingKeys: String, CodingKey {
case reportType, reportData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let reportType = try container.decode(ReportType.self, forKey: .reportType)
switch reportType {
case .simple: self = .simple(try container.decode(SimpleReport.self, forKey: .reportData))
case .advance: self = .advance(try container.decode(AdvancedReport.self, forKey: .reportData))
}
}
}
After decoding the JSON you can switch on reports
for report in response.reports {
switch report {
case .simple(let simpleReport): print(simpleReport)
case .advance(let advancedReport): print(advancedReport)
}
}

JSON decoder for Swift dealing with changing underlying JSON with Array and Dictionary

I am using a third-party API to get data. It is a rather complex payload but I'm experiencing a problem with one return. For this example I'm over-simplifying the structure. This structure actually has 53 entries, 34 of which are structures themselves.
struct MlsItemData: Codable, Hashable {
let mls_id: String
let photos: [MlsItemPhoto]?
let features: [MlsItemFeature]?
let address: MlsItemAddress
let move_in_date: String?
let stories: Int?
let client_flags: MlsItemClientFlags?
let tax_history: [MlsItemTaxHistory]? <-- our propblem child
let new_construction: Bool?
let primary: Bool?
let prop_common: MlsItemPropertyCommon?
There are a whole load of other data objects in this API's results but I'm focusing on one item with the label tax_history. When there is data to be shared the key contains an Array like below.
{
"tax_history": [
{
"assessment": {
"building": null,
"total": 3900,
"land": null
},
"tax": 683,
"year": "2020"
},
{
"assessment": {
"building": null,
"total": 4093,
"land": null
},
"tax": 698,
"year": 2019
}
]
}
When the API has no data to share I was expecting:
"tax_history": [ ]
or
"tax_history": null
or just not in the payload at all. But instead the API is sending:
"tax_history": { }
I'm having difficulty as to how to deal with this in the decoder. Obviously, the built in decoder returns the "Expected to decode Array but found a dictionary instead", but is there a simple way to write a custom decoder for "just" the tax_history key and how would it be written for either getting an Array or an empty dictionary?
Yes, it is possible to decode this unusual payload using JSONDecoder. One way to do so is to use a custom type to represent either the empty or non-empty scenarios, and implement a custom initializer function and attempt to decode both cases to see which one works:
struct TaxHistoryItem: Decodable {
let year: String
// ...
}
enum TaxHistory: Decodable {
case empty
case items([TaxHistoryItem])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let items = try? container.decode([TaxHistoryItem].self) {
self = .items(items)
} else {
struct EmptyObject: Decodable {}
// Ignore the result. We just want to verify that the empty object exists
// and doesn't throw an error here.
try container.decode(EmptyObject.self)
self = .empty
}
}
}
You could create a specific type that holds this array and then write a custom init(from:) for it.
In the init we try to decode the json as an array and if it fails we simply assign an empty array to the property (nil for an optional property is another possible solution but I prefer an empty collection before nil)
struct TaxHistoryList: Codable {
let history: [TaxHistory]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let list = try? container.decode([TaxHistory].self) {
history = list
} else {
history = []
}
}
}
struct TaxHistory: Codable {
let tax: Int
let year: String
// other stuff
}

Decoding JSON structures with minor differences into just one format

I have three API endpoints that result in the same final structure, however the full JSON structure received from the API is a bit different in each of them.
First JSON result
{
"tracks": [{
"name": "Never Gonna Give You Up"
}]
}
Second JSON result
{
"items": [{
"name": "Never Gonna Give You Up"
}]
}
Third JSON result
{
"items": [{
"track": {
"name": "Never Gonna Give You Up"
}
}]
}
I want all of them to look like this
{
"tracks": [{
"name": "Never Gonna Give You Up"
}]
}
For that I'm using three different structures:
First:
struct TopHitsTrackResponse: Decodable {
var tracks: [Track]
}
Second:
struct FavoritesTrackResponse: Decodable {
var tracks: [Track]
enum CodingKeys: String, CodingKey {
case tracks = "items"
}
}
And the third one is the code below.
What I've tried
I have successfully made the first and second JSON results look exactly equal to the wanted result. However, the third one is a bit more complicated for me. Here's what I've tried without success.
struct NestedTrackResponse: Decodable {
let tracks: [Track]
enum CodingKeys: String, CodingKey {
case tracks = "items"
}
enum TrackKeys: String, CodingKey {
case track
}
init(from decoder: Decoder) throws {
let outerContainer = try decoder.container(keyedBy: CodingKeys.self)
let trackContainer = try outerContainer.nestedContainer(keyedBy: TrackKeys.self,
forKey: .tracks)
self.tracks = try trackContainer.decode([Track].self, forKey: .track)
}
struct Track: Decodable {
var name: String
}
}
I'm calling the API with this function
AF.request(urlRequest)
.validate()
.responseDecodable(of: NestedTrackResponse.self) { response in
// It's always resulting in fatal error
guard let data = response.value else {
fatalError("Error receiving tracks from API.")
}
}
// - `AF` is Alamofire, but I've already tried not using it,
// and the error persists
// - `urlRequest` is just the URL of the API and the API key,
// doesn't really matter for this problem
And getting this error
Expected to decode Dictionary<String, Any> but found an array instead.
You have to write a custom initializer with if let expressions to distinguish the cases.
struct Response : Decodable {
let tracks : [Track]
private enum CodingKeys : String, CodingKey { case items, tracks }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let tracks = try? container.decode([Track].self, forKey: .items) {
self.tracks = tracks
} else if let tracks = try? container.decode([Track].self, forKey: .tracks) {
self.tracks = tracks
} else if let items = try? container.decode([Item].self, forKey: .items) {
self.tracks = items.map(\.track)
} else {
throw DecodingError.dataCorruptedError(forKey: .items, in: container, debugDescription: "Unsupported JSON structure")
}
}
}
struct Item : Decodable {
let track : Track
}
struct Track : Decodable {
let name : String
}
As I said in the other answer, recommending to stick to the JSON structure that you receive from the API, this will prevent many headaches in future, as well as having a well-defined networking layer.
struct Track: Decodable {
var name: String
}
struct TopHitsTrackResponse: Decodable {
var tracks: [Track]
}
struct FavoritesTrackResponse: Decodable {
var items: [Track]
}
// though you should name this by the API name,
// like with the other two
struct NestedTrackResponse: Decodable {
var items: [Item]
struct Item: Decodable {
var track: Track
}
}
Basically the above is your networking layer. Now, comes the business layer, which will try to extract the tracks from each kind of response.
You could implement it with protocols, but functions work as well (if not even better):
func tracks(from response: TopHitsTrackResponse) -> [Track] {
response.tracks
}
func tracks(from response: FavoritesTrackResponse) -> [Track] {
response.items
}
func tracks(from response: NestedTrackResponse) -> [Track] {
response.items.map(\.track)
}
Then, in your API callbacks, you simply call tracks(from:), in order to extract the most wanted results.

Swift decode object with some common elements, and some changing

There are a few different JSON objects that could be returned from a single endpoint. There will always be some elements in the object, including a type, and I know all the types. But then the different objects could have some different elements. Here's some example JSON
// Story.json
{
"id": 1,
"time": 1314211127,
"type": "story",
"title": "Awesome story",
"comment_count": 36
}
// Ad.json
{
"id": 2,
"time": 1316142113127,
"type": "ad",
"image_url": "https://someurl.com/",
"tracking_id": 67814
}
// Comment.json
{
"id": 3,
"time": 131448227,
"type": "comment",
"text": "A comment",
"parent_id": 1
}
So you can see that there's comment elements like id, time, and comment. However, based on the type, the additional elements are different. I'm getting confused trying to decode this. So far I have:
enum ItemType: String, Decodable {
case story
case ad
case comment
}
protocol Item: Decodable {
var id: Int { get }
var time: Int? { get }
var type: ItemType { get }
}
struct AnyItem: Item {
let id: Int
let time: Int?
let type: ItemType
private enum CodingKeys: String, CodingKey {
case id, time, type
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
time = try container.decodeIfPresent(Int.self, forKey: .time)
type = try container.decode(ItemType.self, forKey: .type)
// Then I'm assuming switch on the type here
switch type {
// don't know where to go from here...
}
}
}
I guess I need other types conforming to Item such as StoryItem, AdItem, CommentItem. Then, because the endpoint can return all of these items in one response, I can store it in an array of type [Item]. I just can't figure out how to decode all those types in one response.
Any help appreciated.
An easy method would be to use optionals for the optional keys and they'll be ignored while decoding. Since you already have the type, you can know for sure which keys are gonna be present or you can just unwrap them.
struct Item: Codable {
let id: Int
let time: Int
let type: ItemType
let text: String?
let title: String? // and all other optional properties
}

Decoding JSON array of different types in Swift

I'm trying to decode the following JSON Object
{
"result":[
{
"rank":12,
"user":{
"name":"bob","age":12
}
},
{
"1":[
{
"name":"bob","age":12
},
{
"name":"tim","age":13
},
{
"name":"tony","age":12
},
{
"name":"greg","age":13
}
]
}
]
}
struct userObject {
var name: String
var age: Int
}
Basically a JSON Array with two different object types
{ "rank":12, "user": {userObject} }
and a
"1" : array of [userObjects]
struct data: Decodable {
rank: Int
user: user
1: [user] <-- this is one area Im stuck
}
Thanks in advance
Just for fun:
First you need structs for the users and the representation of the first and second dictionary in the result array. The key "1" is mapped to one
struct User : Decodable {
let name : String
let age : Int
}
struct FirstDictionary : Decodable {
let rank : Int
let user : User
}
struct SecondDictionary : Decodable {
let one : [User]
private enum CodingKeys: String, CodingKey { case one = "1" }
}
Now comes the tricky part:
First get the root container.
Get the container for result as nestedUnkeyedContainer because the object is an array.
Decode the first dictionary and copy the values.
Decode the second dictionary and copy the values.
struct UserData: Decodable {
let rank : Int
let user : User
let oneUsers : [User]
private enum CodingKeys: String, CodingKey { case result }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var arrayContainer = try container.nestedUnkeyedContainer(forKey: .result)
let firstDictionary = try arrayContainer.decode(FirstDictionary.self)
rank = firstDictionary.rank
user = firstDictionary.user
let secondDictionary = try arrayContainer.decode(SecondDictionary.self)
oneUsers = secondDictionary.one
}
}
If this code is preferable over traditional manual JSONSerialization is another question.
If your JSON format is given then you are pretty much out of luck, since you will most likely have to parse your array as [Any] which is, to put it mildly, not very useful. If on the other hand you are able to modify the format of the JSON you should start from the other direction. Define your desired Swift object and encode it using JSONEncoder.encode(...) in order to quickly determine how your JSON should look like in order to make it parse in as typed a way as possible.
This approach will easily half your JSON handling code as your web service protocol will end up being structured much better. This will likely improve the structure of the overall system since it will yield a much more stable communication protocol.
Sadly enough this approach is not always possible which is when things get messy. Given your example you will be able to parse your code as
let st = """
{
"result":[
{
"rank":12,
"user":{
"name":"bob",
"age":12
}
},
{
"1":[
{
"name":"bob","age":12
},
{
"name":"tim","age":13
},
{
"name":"tony","age":12
},
{
"name":"greg","age":13
}
]
}
]
}
"""
let jsonData1 = st.data(using: .utf8)!
let arbitrary = try JSONSerialization.jsonObject(with: jsonData1, options: .mutableContainers)
This will let you access your data with a bunch of casts as in
let dict = arbitrary as! NSDictionary
print(dict["result"])
you get the idea. not very useful as you would very much like to use the Codable protocol as in
struct ArrayRes : Codable {
let result : [[String:Any]]
}
let decoder1 = JSONDecoder()
do {
let addrRes = try decoder.decode(ArrayRes.self, from: jsonData1)
print(addrRes)
} catch {
print("error on decode: \(error.localizedDescription)")
}
Unfortunately this does not work since Any is not Codable for slightly obvious reasons.
I hope you are able to change your JSON protocol since the current one will be the root cause of lot of messy code.