Need help decoding Json that contains an array - json

I need to do the following :
Define two Swift classes to decode the JSON string
Decode the JSON string to get the objects of the two classes
This is the JSON I have to decode :
{“status":200,"holidays":[{"name":"Thanksgiving","date":"2017-10-09","observed":"2017-10-09","public":false}]}
I have tried creating two classes already and all I get back is nothing when calling the class in the main class
class HolidayItems : Decodable {
let name : String?
let date : String?
let observed: String?
let `public` : Bool?
private enum CodingKeys: String, CodingKey {
case name
case date
case observed
case `public`
}
required init(from decoder:Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
date = try container.decode(String.self, forKey: .date)
observed = try container.decode(String.self, forKey: .observed)
`public` = try container.decode(Bool.self, forKey: .`public`)
}
} // HolidayItems
class HolidayAPI: Decodable {
let status: HolidayItems
// let holiday :[HolidayItems]
func getHolidayName() -> String {
return status.name ?? "no advice, server problem"
}
func getAdviceNo() -> String {
return status.date ?? ""
}
private enum CodingKeys: String, CodingKey {
case status
case holiday = "items"
}
required init(from decoder:Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(HolidayItems.self, forKey: .status)
// holiday = try container.decode(HolidayItems.self, forKey: .holiday)
}
}
This is the result I'm suppose to get :
Optional("Thanksgiving")
Optional("2017-10-09")
and I get nothing in return

Your response is on root level just object with status of type Int and one array of another objects
Note
you don't have to implement your custom CodingKey
you don't need custom init with Decoder
you can have struct for your models
you can rename HolidayItems to Holiday
struct HolidayAPI: Decodable {
let status: Int
let holidays: [Holiday]
}
struct Holiday: Decodable {
let name, date, observed: String
let `public`: Bool
}
Then when you need to get certain holiday item, just get certain element of holidays
decodedResponse.holidays[0].name

Related

How should I decode a json object using JSONDecoder if I am unsure of the keys

I have an api response in the following shape -
{
"textEntries":{
"summary":{
"id":"101e9136-efd9-469e-9848-132023d51fb1",
"text":"some text",
"locale":"en_GB"
},
"body":{
"id":"3692b0ec-5b92-4ab1-bc25-7711499901c5",
"text":"some other text",
"locale":"en_GB"
},
"title":{
"id":"45595d27-7e06-491e-890b-f50a5af1cdfe",
"text":"some more text again",
"locale":"en_GB"
}
}
}
I'd like to decode this via JSONDecoder so I can use the properties. The challenge I have is the keys, in this case summary,body and title are generated elsewhere and not always these values, they are always unique, but are based on logic that takes place elsewhere in the product, so another call for a different content article could return leftBody or subTitle etc.
The model for the body of these props is always the same however, I can expect the same fields to exist on any combination of responses.
I will need to be able to access the body of each key in code elsewhere. Another API response will tell me the key I need though.
I am not sure how I can handle this with Decodable as I cannot type the values ahead of time.
I had considered something like modelling the body -
struct ContentArticleTextEntries: Decodable {
var id: String
var text: String
var locale: Locale
}
and storing the values in a struct like -
struct ContentArticle: Decodable {
var textEntries: [String: ContentArticleTextEntries]
private enum CodingKeys: String, CodingKey {
case textEntries
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.textEntries = try values.decode(ContentArticleTextEntries.self, forKey: .textEntries)
}
}
I could them maybe use a subscript elsewhere to access property however I do not know how to decode into this shape as the above would not work.
So I would later access like textEntries["body"] for example.
I also do no know if there is a better way to handle this.
I had considered converting the keys to a 'type' using an enum, but again not knowing the enum cases ahead of time makes this impossible.
I know textEntries this does not change and I know id, text and locale this does not change. It is the keys in between this layer I do not know. I have tried the helpful solution posted by #vadian but cannot seem to make this work in the context of only needing 1 set of keys decoded.
For the proposed solution in this answer the structs are
struct ContentArticleTextEntries: Decodable {
let title : String
let id: String
let text: String
let locale: Locale
enum CodingKeys: String, CodingKey {
case id, text, locale
}
init(from decoder: Decoder) throws {
self.title = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.text = try container.decode(String.self, forKey: .text)
let localeIdentifier = try container.decode(String.self, forKey: .locale)
self.locale = Locale(identifier: localeIdentifier)
}
}
struct ContentArticle: TitleDecodable {
let title : String
var elements: [ContentArticleTextEntries]
}
struct Container: Decodable {
let containers: [ContentArticle]
init(from decoder: Decoder) throws {
self.containers = try decoder.decodeTitledElements(ContentArticle.self)
}
}
Then decode Container.self
If your models are like,
struct ContentArticle: Decodable {
let textEntries: [String: ContentArticleTextEntries]
}
struct ContentArticleTextEntries: Decodable {
var id: String
var text: String
var locale: String
}
Then, you can simply access the data based on key like,
let response = try JSONDecoder().decode(ContentArticle.self, from: data)
let key = "summary"
print(response.textEntries[key])
Note: No need to write enum CodingKeys and init(from:) if there is no special handling while parsing the JSON.
Use "decodeIfPresent" variant method instead of decode method also you need to breakdown the ContentArticleTextEntries dictionary into individual keys:
struct ContentArticle: Decodable {
var id: String
var text: String?
var locale: String?
private enum CodingKeys: String, CodingKey {
case id, text, locale
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? ""
self.text = try container.decodeIfPresent(String.self, forKey: .text)
self.locale = try container.decodeIfPresent(String.self, forKey: .locale)
}
}

How to dynamically add properties to a constant Decodable object in Swift?

Background
Basically I got an api that returns something like this:
"order_detail": [
{
"id": 6938,
"order_id": 6404,
"item_price": "4",
..
"item": {
"id": 12644,
"ref": "Iced Caffe Americano",
"short_description": "",
..
and in my decodable obj i got this
public struct OrderDetail: Decodable {
public let id: Int
public let order_id: Int
public let item_price: String?
..
public let item: Item?
and
public struct Item: Decodable {
public var id: Int
public var ref: String?
public var short_description: String?
The problem is that somewhere else in the code, there is a method that's expecting the Item object to have item_price.
Question
What I want to do is swizzle or mutate this constant Item object and dynamically add item_price property to it.. How can I do that?
Workarounds, other solutions
1. Change json
I know there are many other solutions to this same problem (I'm working on it as we speak, which is simply modifying the api endpoint to suit my needs).. but again that option is not always possible (ie suppose the backend team is separate)
2. Change the function expectation
That is also possible, but also not inexpensive as this function is used in many other places in the app which I don't potentially have control over
If you want to add a property to a Decodable type that's not part of its JSON representation, so simply need to declare a CodingKey conformant type and leave out the specific property name so that the automatically synthesised init(from decoder:Decoder) initialiser will know not to look for that value in the JSON.
Btw you should also conform to the Swift naming convention (lowerCamelCase for variable names) and use CodingKey to map the JSON keys to the property names.
public struct Item: Decodable {
public var id: Int
public var ref: String?
public var shortDescription: String?
public var itemPrice: String? // or whatever else its type needs to be
private enum CodingKeys: String, CodingKey {
case id, ref, shortDescription = "short_description"
}
}
This is one way to achieve this
Take over the initialization of Item in OrderDetail decoding.
struct OrderDetail: Decodable {
let id: Int
let orderId: Int
let itemPrice: String?
let item: Item
private enum OrderDetailCodingKey: CodingKey {
case id
case orderId
case itemPrice
case item
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: OrderDetailCodingKey.self)
self.id = try container.decode(Int.self, forKey: .id)
self.orderId = try container.decode(Int.self, forKey: .orderId)
let itemPrice = try container.decode(String?.self, forKey: .itemPrice)
self.itemPrice = itemPrice
self.item = try Item(from: decoder, itemPrice: itemPrice)
}
}
Use a custom initializer to create your item.
struct Item: Decodable {
let id: Int
let ref: String?
let shortDescription: String?
let itemPrice: String?
private enum ItemCodingKeys: CodingKey {
case id
case ref
case shortDescription
}
init(from decoder: Decoder, itemPrice: String?) throws {
let container = try decoder.container(keyedBy: ItemCodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.ref = try? container.decode(String.self, forKey: .ref)
self.shortDescription = try? container.decode(String.self, forKey: .shortDescription)
self.itemPrice = itemPrice
}
}
You can call the following function to test the functionality:
private func test() {
let json = """
{"id":6938,"orderId":6404,"itemPrice":"4","item":{"id":12644,"ref":"Iced Caffe Americano","shortDescription":""}}
"""
let data = json.data(using: .utf8)
let decoder = JSONDecoder()
if let data = data {
do {
let order = try decoder.decode(OrderDetail.self, from: data)
print(order)
} catch let jsonError {
os_log("JSON decoding failed [%#]", String(describing: jsonError))
}
} else {
os_log("No data found")
}
}

Swift 4 decoding doubles from JSON

I though I had this concept nailed!
I am sending a JSON which contains a double.
{"elementName":"Security:Driver","element_Cost":"650"}
I've created CodingKeys and a decoder extension but I still get a Type Mismatch error when I send the data.
struct ElementCosts: Content {
let elementName: String
let elementCost: Double
enum CodingKeys: String, CodingKey {
case elementCost = "element_Cost"
case elementName
}
}
extension ElementCosts: Decodable {
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
elementCost = try values.decode(Double.self, forKey: .elementCost)
elementName = try values.decode(String.self, forKey: .elementName)
}
}
Looking at some of the other posts here I cannot see what I've done wrong.
I've tried to change the Data type to Int but still has the same issue.
Any ideas?
"650" is a string, not a number.
You can parse it like this
let elementCostString = try values.decode(String.self, forKey: .elementCost)
elementConst = Double(elementCostString) ?? 0
Or change it to be a String on your model, whichever works better for you.

swift4 encoding decoding for model class of nested json parsing

I have a model class of swift which was created based on a nested json response, it follows like below
struct RootClass : Codable {
let details : String?
let itemCount : Int?
let list : [List]?
enum CodingKeys: String, CodingKey {
case details = "Details"
case itemCount = "ItemCount"
case list = "List"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
details = try values.decodeIfPresent(String.self, forKey: .details)
itemCount = try values.decodeIfPresent(Int.self, forKey: .itemCount)
list = try values.decodeIfPresent([List].self, forKey: .list)
}
}
struct List : Codable {
let companyID : Int?
let employeeCount : Int?
let employeeUser : EmployeeUser?
enum CodingKeys: String, CodingKey {
case companyID = "CompanyID"
case employeeCount = "EmployeeCount"
case employeeUser = "EmployeeUser"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
companyID = try values.decodeIfPresent(Int.self, forKey: .companyID)
employeeCount = try values.decodeIfPresent(Int.self, forKey: .employeeCount)
employeeUser = try EmployeeUser(from: decoder)
}
}
struct EmployeeUser : Codable {
let mobileNumber : String?
let name : String?
enum CodingKeys: String, CodingKey {
case mobileNumber = "MobileNumber"
case name = "Name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mobileNumber = try values.decodeIfPresent(String.self, forKey: .mobileNumber)
name = try values.decodeIfPresent(String.self, forKey: .name)
}
}
and my json response is
{
"Details": null,
"List": [
{
"CompanyID": 140,
"EmployeeUser": {
"Name": " raghu2",
"MobileNumber": "8718718710"
},
"EmployeeCount": 0
},
{
"CompanyID": 140,
"EmployeeUser": {
"Name": "new emp reg",
"MobileNumber": "1"
},
"EmployeeCount": 0
}
],
"ItemCount": 0
}
I am trying to parse it like
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(RootClass.self, from: data)
print(gitData.itemCount ?? "")
print(gitData.list![0].employeeUser?.mobileNumber ?? "")
}
catch let err {
print("Err", err)
}
I am able to get the values of root class and list but I am getting nil values under employee user section.
Your code a few problems:
All your keys are optional. The vendor API will tell you what keys are always present and which one are optional. Follow that.
decodeIfPresent will silently fail if it cannot decode a key. When debugging your app, you want things to fail with a bang so you can fix the error before going to production.
You wrote way more code than needed. All those init(from decoder: ) functions are not needed. One one did cause your problem.
Your problem was caused by this line:
struct List : Codable {
init(from decoder: Decoder) throws {
...
employeeUser = try EmployeeUser(from: decoder)
}
}
You are asking Swift to decode to same JSON to a List and a EmployeeUser object. Obviously, that's not valid. But when you decode list inside RootClass, you call decodeIfPresent:
// In Rootclass
list = try values.decodeIfPresent([List].self, forKey: .list)
This call silently failed and you never knew what the problem was!
Solution
Change how you initialize employeeUser to this:
employeeUser = try values.decodeIfPresent(EmployeeUser.self, forKey: .employeeUser)
But the most elegant solution is to delete all those init(from decoder: ). The compiler will synthesize them for you.
And finally, fix those optionals!

Type Mismatch error parsing JSON with Swift Decodable

When I try to decode JSON I get the error:
(Error: typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [sweetyanime.Video.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).video, sweetyanime.iD.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).ID, sweetyanime.other.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).CountOfVideos], debugDescription: "Expected to decode String but found a dictionary instead.", underlyingError: nil)
1) JSON:
{ "video":{
"ID":{
"Name":"NameOfAnime",
"Describe":"SomeDescribe",
"Image":"https://firebasestorage.googleapis.com/v0/b/sweety-anime-e6bb4.appspot.com/o/main.png?alt=media&token=042a2dad-8519-4904-9ba3-262c2c962434",
"CountOfVideos":{
"1Series":"https://firebasestorage.googleapis.com/v0/b/sweety-anime-e6bb4.appspot.com/o/message_movies%252F12323439-9729-4941-BA07-2BAE970967C7.movalt=media&token=978d8b3a-7aad-468f-87d4-2b587d616720"
}
} } }
2) Swift code:
let jsonUrl = "file:///Users/tima/WebstormProjects/untitled/db.json"
guard let url = URL(string: jsonUrl) else { return }
URLSession.shared.dataTask(with: url) { (data, reponse, error) in
guard let data = data else {return}
do {
let video = try
JSONDecoder().decode(Video.self, from: data)
print(video.video.ID.Name)
} catch let jsonErr {
print("Error: ", jsonErr)
}
}.resume()
3) Video.swift
struct Video: Decodable {
private enum CodingKeys : String, CodingKey { case video = "video"
}
let video: iD
}
struct iD: Decodable {
private enum CodingKeys : String, CodingKey { case ID = "ID" }
let ID: other
}
struct other: Decodable {
private enum CodingKeys : String, CodingKey {
case Name = "Name"
case Describe = "Describe"
case Image = "Image"
case CountOfVideos = "CountOfVideos"
}
let Name: String
let Describe: String
let Image: String
let CountOfVideos: String
}
Let's put some line breaks in your error message to make it understandable:
(Error: typeMismatch(Swift.String,
Swift.DecodingError.Context(codingPath: [
sweetyanime.Video.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).video,
sweetyanime.iD.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).ID,
sweetyanime.other.(CodingKeys in _D1045E05CDE474AEBA8BDCAF57455DC3).CountOfVideos],
debugDescription: "Expected to decode String but found a dictionary instead.",
underlyingError: nil)
So it was trying to decode the value for “CountOfVideos”. It expected a String but it found a dictionary.
You need to either define a struct corresponding to the “CountOfVideos” dictionary (it appears to contain one key, “1Series”, with a string value), or you need to remove the CountOfVideos property from your other struct so you don't try to decode it at all.
My I think you should implement the init(from decoder) initializer that is part of Decodable. It gives a lot more flexibility on how to handle nested JSON than you get from the automatic implementation. One possible implementation would be something like:
struct Video: Decodable {
let name: String
let describe: String
let image: String
let countOfVideos: String
private enum VideoKey: String, CodingKey {
case video = "video"
}
private enum IDKey: String, CodingKey {
case id = "ID"
}
private enum CodingKeys: String, CodingKey {
case name = "Name"
case describe = "Describe"
case image = "Image"
case countOfVideos = "CountOfVideos"
}
private enum VideoCountKey: String, CodingKey {
case videoCount = "1Series"
}
init(from decoder: Decoder) throws {
let videoContainer = try decoder.container(keyedBy: VideoKey.self)
let idContainer = try videoContainer.nestedContainer(keyedBy: IDKey.self, forKey: .video)
let valuesContainer = try idContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .id)
self.name = try valuesContainer.decode(String.self, forKey: .name)
self.describe = try valuesContainer.decode(String.self, forKey: .describe)
self.image = try valuesContainer.decode(String.self, forKey: .image)
let videoCountContainer = try valuesContainer.nestedContainer(keyedBy: VideoCountKey.self, forKey: .countOfVideos)
self.countOfVideos = try videoCountContainer.decode(String.self, forKey: .videoCount)
}
This works with your example data in a Playground. I really don't know if you wanted the key or the value for your videoCount. (Neither looks like a count of videos to me. Generally, you just need to define a CodingKey enum for every level of your nested JSON and you can decode individual values as you see fit. (Note, since I've only seen one example of your data, this may break on some stuff in whatever API you're working with, so modify/take the ideas and rewrite it as needed)