Swift Codable decode changing JSON and ignoring some fields - json

I have some REST API, which returns JSON. This API is used by more than one service (mobile, web,...) and it returns JSON that has more fields inside than I need and some of those additional fields can be changed on weekly basis.
For example I have JSON like that:
{ "name": "Toyota Prius", "horsepower": 134, "mileage": 123241,
"manufactured": 2017, "location": "One city", "origin": "Japan",
"convert": true //more properties follows... }
In mobile app I would only need to decode fields: name, horsepower and manufactured date, the rest can be ignored. Also some fields like origin haven't existed before but was added few days ago.
Question is how to write bulletproof decoder that would not break if there are some changes in JSON that do not affect my model in app? Also I have no word in output JSON from server and I cannot affect it. I just get data and I have to work with what I get.
I have wrote some experimental code for playground (car isn't printed out):
import UIKit
struct Car: Codable
{
let name: String
let horsePower: Int
let selected: Bool //this is used as additional property inside my app to mark selected cars
let index: Int //this is used as additional property inside my app for some indexing purposes
enum CodingKeys: String, CodingKey
{
case name
case horsePower = "horsepower"
case selected
case index
}
init(from decoder: Decoder) throws
{
let values = try decoder.container(keyedBy: CodingKeys.self)
selected = false
index = 0
name = try values.decode(String.self, forKey: .name)
horsePower = try values.decode(Int.self, forKey: .horsePower)
}
}
let json = "{\"name\": \"Toyota Prius\",\"horsepower\": 134, \"convert\", true}"
let data = json.data(using: .utf8)
if let car = try? JSONDecoder().decode(Car.self, from: data!)
{
//does not print... :(
print(car)
}
I would like to get car printed in my example but mostly have a working proof code that will not break if JSON get changed.
Also is there a way to get an decoding error description somehow?
I know there are a lot of things in apple documentation, but mostly it is just too confusing to me and I couldn't find any useful examples for my problem.

First of all never try? JSONDecoder... , catch always the error and print it. DecodingErrors are extremely descriptive. They tell you exactly what is wrong and even where.
In your example you will get
"The given data was not valid JSON. ... No value for key in object around character 52."
which is the wrong comma (instead of a colon) after convert\"
To decode only specific keys declare the CodingKeys accordingly and delete the init method. selected and index are most likely supposed to be mutable so declare them as variable with a default value.
If the backend changes the JSON structure you'll get an error. The decoding process will break anyway regardless of the parsing API.
struct Car: Codable
{
let name: String
let horsePower: Int
let convert : Bool
var selected = false
var index = 0
enum CodingKeys: String, CodingKey {
case name, horsePower = "horsepower", convert
}
}
let json = """
{"name":"Toyota Prius","horsepower":134,"convert":true}
"""
let data = Data(json.utf8)
do {
let car = try JSONDecoder().decode(Car.self, from: data)
print(car)
} catch { print(error) }

Related

JSON to dict with class

I decide some JSON and try to typecast it to a dictionary of String: classy and it fails. I have found that often the reason I have trouble doing something is because of a misunderstanding of how Swift works, so here is what I want to happen. Feel free to tell me that I am doing it wrong and if I do it this way all will be wonderful.
I want my data to live between runs of the app so I have to save the data to storage between runs. I have an object, data and associated code, and I have places where changes I make to a copy should reflect back to the original so it is a class. I have a bunch of these objects and most of the time I pick the one I want based on an id that is an integer. An array is not good since it would be a sparse array cause come ids are not used. I came up with a dictionary with a key of the id and data of the structure. I turned the key from an Int to a String, by changing the Int id to a String, cause converting a dictionary to JSON is MUCH easier for a key that is a string. I save the JSON string. When the app starts again I read the string in and convert the JSON string to Any. Then I typecast the result to the desired dictionary. This is where it fails. The cast does not work. In my Googling the samples I found said this should work.
Here is my code:
class Y: Codable, Hashable {
var a: String = "c"
static func ==(lhs: Y, rhs: Y) -> Bool {
return lhs.a == rhs.a
}
func hash(into hasher: inout Hasher) {
hasher.combine(a)
}
}
struct ContentView: View {
var body: some View {
VStack {
Button ("Error") {
var y = Y()
var yDict = [String: Y]()
yDict["0"] = y
do {
let encodedData = try JSONEncoder().encode(yDict)
let jsonString = String(data: encodedData, encoding: .utf8)
let decoded = try JSONSerialization.jsonObject(with: encodedData, options: [])
if let yyDictDec = decoded as? [String:Y] {
print("yDict after decide")
print (yyDictDec)
}
} catch {
print(error.localizedDescription)
}
print("x")
}
}
}
}
In this code the if yyDictDec = is failing, I think, cause the prints after it never happen. I can cast it as [String, Any] but I really need it to be my class.
My problem is in the convert JSON back to the dictionary. I feel I am missing something fairly simple.
DonĀ“t use JSONSerialization use JsonDecoder and decode it to the the type it was before encoding. e.g.:
let decoded = try JSONDecoder().decode([String: Y].self, from: encodedData)

JSON Parsing using Decodable protocol

I have json below for which I want to parse/assign values from
{
"Rooms":[
{
"id":"100",
"title":"CS Classroom",
"description":"Classroom for Computer science students",
"capacity":"50"
},
{
"id":"101",
"title":"Mechanical Lab",
"description":"Mechanical Lab work",
"capacity":"50"
},
{
"id":"108",
"title":"Computer Lab",
"description":"Computer Lab work",
"capacity":"50"
}
]
}
This json is of type [Dictionary: Dictonary] which has only key "Rooms"
While creating struct should I create
struct RoomsInfo: Decodable {
let rooms: Rooms
}
struct Rooms {
let id: String
let title: String
let description: String
let capacity: String
}
My 1st Question is: Since I have only Rooms key , Is there a possiblity to create just one struct instead of two ?
My 2nd Question is: What if my json has keys as "Rooms1", "Rooms2", "Rooms3", "Rooms4"... in this case can i create structure which confirms to decodable or do i need to parse it manually?
Please advice
For the first question, you have a key called Room so it has to decode that key,
is it possible to not have it sure, instead of parsing that JSON data first call out the value of that key JSON["Rooms"], and parse what inside as a [Room].self ,
For the second question if the count is unlimited, as if you don't know how much Room key count are going to be, the Decoder abilities are limited then, however you can always map out the values as Dictionary and then decode the values as Room without caring about the key, this trick will do but you will abandon the original Key.
Update for the second case:
Check out this code below.
typealias jsonDictionary = [String: Any]
let jsonData = json.data(using: .utf8)! // converting test json string to data
var arrayOfRooms: [Room] = []
do {
let serialized = try JSONSerialization.jsonObject(with: jsonData, options: []) // serializing jsonData to json object
if let objects = serialized as? [String: Any] { //casting to dictionary
for key in objects.keys { //looping into the keys rooms (n) number
let rooms = objects[key] // getting those rooms by key
let data = try JSONSerialization.data(withJSONObject: rooms!, options: []) //converting those objects to data again to parse
var myRoom = try! JSONDecoder().decode([Room].self, from: data) // decoding each array of rooms
arrayOfRooms.append(contentsOf: myRoom) // appending rooms to main rooms array declared on top
print("Data", data) // just to check
}
print("MY Array Of Rooms Count \(arrayOfRooms.count)")
} else {
print("nil")
}
} catch {
}
Answer #1: Yes, it's possible with nestedContainers but the effort is greater than the benefit.
Answer #2: Decode the dictionary as [String:Room] or use custom coding keys described in this answer

decoding JSON array or dictionary error swift

This is my first time taking a shot at Codable/Decodable and i would like to decode a JSON. I am attempting to access the "name" and "description" keys within the events array. Below is a snippet of the JSON - im getting this error within my code
"Expected to decode Dictionary but found an array instead."
"pagination": {
"page_number": 1,
"page_size": 50,
"continuation": "eyJwYWdlIjogMn0",
"has_more_items": true
},
"events": [
{
"name": {
"text": "Genesis Part 4",
"html": "Genesis Part 4"
},
"description": {
"text": "Wednesday, June 6-June 27, 2018\n12:00-2:15 PM, Eastern Time\n\u00a0\nCOED\n\u00a0\nBible Study Leader:\u00a0Nicki Cornett\n\u00a0\nContact:NickiCornett#gmail.com\n\u00a0\nGenesis Part 4 -\u00a0Wrestling with God - A Study on Isaac, Jacob, and Esau- Precept Workbook (NASB)\n\u00a0\nGod renews His covenant promise with Abraham through Isaac and Jacob.
Here is how i went about decoding - (NOTE - "description" is not here yet because i having an issue working into the events array to access name & descritption
struct Eventbrite: Decodable {
private enum CodingKeys : String, CodingKey { case events = "events", name = "name"}
let events: [String:[String]]
let name: [String:[String]]
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {return}
do {
let eventbriteData = try JSONDecoder().decode(Eventbrite.self, from: data)
print(eventbriteData.name)
name is clearly not in the scope of pagination and events (note the {}) and is a regular [String:String] dictionary which can be decoded into another struct.
Decode this (as description is incomplete I left it out), you don't need CodingKeys:
struct Eventbrite: Decodable {
let events: [Event]
}
struct Event: Decodable {
let name: Name
// let description: [String:String]
}
struct Name : Decodable {
let text, html : String
}
Right so Decodable is actually pretty smart in that you really don't need to write any code to do the decoding yourself. You just have to make sure you match the JSON structure (and make structs that also conform to Decodable for any nested objects). In other words, instead of having variables as dictionaries, make them their own Decodable struct.
So for example:
struct EventBrite: Decodable {
let pagination: Pagination
let events: [Event]
}
struct Pagination: Decodable {
let page_number: Int
let page_size: Int
let continuation: String
let has_more_items: Bool
}
struct Event: Decodable {
let name: EventName
let description: EventDescription
}
struct EventName: Decodable {
let name: String
let html: String
}
etc...
Something else that's important here is if a key or property is not guaranteed to be returned (like let's say that the EventName doesn't always have an html value that comes back from the server you can easily just mark that value as optional. So something like:
struct EventName: Decodable {
let name: String
let html: String?
}
Another side note, you actually messed up your dictionary type declarations. You'll notice that event is actually of type [String: [String: String]] since the key is a string and the values seem to always be dictionary. And name is [String: String]. Which is not what you had them down as in your original question.
When the values can be different like with pagination you'll want to do something like [String: Any] so just be careful about that.
HOWEVER The approach I suggested I think is better than having properties be dictionaries. For one you don't have to worry about declaring the type of dictionary it is (which you made some small errors on). But more importantly when each dictionary just becomes its own clearly defined struct and you don't have to worry about remembering or looking up the keys. Dot syntax/auto complete will automatically tell you what there can be! (And no casting when your value is of type Any or AnyObject!)
Also definitely use structs for all these as I once benchmarked performance and measured structs efficiency on the order of magnitude of millions of times more efficient than classes. Just a FYI.

Ignoring not supported Decodables

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

Swift 4 Codable - Bool or String values

Looking for some input as to how you would handle the scenario I recently ran into.
I have been using Swift 4s Codable with success but today noticed a crash that I didn't expect. The API that I am working with, says it returns a boolean for the key manage_stock.
My stubbed struct looks like:
struct Product: Codable {
var manage_stock: Bool?
}
That works fine, the problem is that the API sometimes returns a string instead of a boolean.
Because of this, my decode fails with:
Expected to decode Bool but found a string/data instead.
The string only ever equals "parent" and I want it to equate to false.
I am also fine with changing my struct to var manage_stock: String? if that makes things easier to bring the JSON data in from the API. But of course, if I change that, my error just changes to:
Expected to decode String but found a number instead.
Is there a simple way to handle this mutation or will I need to lose all the automation that Codable brings to the table and implement my own init(decoder: Decoder).
Cheers
Since you can't always be in control of the APIs you're working with, here's one simple way to solve this with Codable directly, by overriding init(from:):
struct Product : Decodable {
// Properties in Swift are camelCased. You can provide the key separately from the property.
var manageStock: Bool?
private enum CodingKeys : String, CodingKey {
case manageStock = "manage_stock"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.manageStock = try container.decodeIfPresent(Bool.self, forKey: .manageStock)
} catch DecodingError.typeMismatch {
// There was something for the "manage_stock" key, but it wasn't a boolean value. Try a string.
if let string = try container.decodeIfPresent(String.self, forKey: .manageStock) {
// Can check for "parent" specifically if you want.
self.manageStock = false
}
}
}
}
See Encoding and Decoding Custom Types for more info on this.