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.
Related
I have a service that returns an array of objects in this form:
{
result: [
{
objectId: "id",
type: "objectType",
content: { ... }
}, ...
]
}
The content depends on the object type. I've tried building up a custom decoder this way (ContentItem is a protocol for ClassA, B and so on):
struct ContentDataItem: Decodable {
var objectId: String?
var type: String?
var content: ContentItem?
private enum CodingKeys: String, CodingKey {
case objectId
case type
case content
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
objectId = try container.decode(String.self, forKey: .objectId)
type = try container.decode(String.self, forKey: .type)
if let type = type {
switch type {
case "typeA":
content = try container.decode(ClassA.self, forKey: .content)
case "typeB":
content = try container.decode(ClassB.self, forKey: .content)
...
}
}
}
}
This works, but I get a high cyclomatic complexity (I'm aiming for sub-10, but I have 14 different classes for content). So I've tried changing my approach, making ContentItem the superclass and changing the init into something like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
objectId = try container.decode(String.self, forKey: .objectId)
let type = try container.decode(String.self, forKey: .type)
let itemTypes: [String: ContentItem.Type] = [
"typeA": ClassA.self,
"typeB": ClassB.self,
...
]
guard let type = type, let contentItemType = itemTypes[type] else { return }
content = try container.decode(contentItemType, forKey: .content)
}
This reduces the cyclomatic complexity as I wanted, but it doesn't work anymore because the decode only returns objects of type ContentItem (the superclass), not the specific ClassA, ClassB that I want. Is there a way to make this approach work? What is wrong with it?
And more importantly, is there a more efficient way to parse this object?
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)
}
}
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
I'm fairly new to dealing with JSON data in Swift and I am trying to subclass some products. I don't mean to code dump, but I want to give you the whole picture. I have three errors that say the same thing: Errors thrown from here are not handled They occur in required init. Thanks in advance. Here's the code:
import UIKit
class Product: Decodable {
var category: String = ""
var material: String = ""
init() {
}
}
class TelephoneWithCord: Product {
var sku: Double
var isNew: Bool
private enum CodingKeys: String, CodingKey {
case sku = "sku"
case isNew = "isNew"
}
required init(from decoder: Decoder) {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.sku = try container.decode(Double.self, forKey: .sku)
self.isNew = try container.decode(Bool.self, forKey: .isNew)
}
}
let json = """
{
"category" : "home",
"material" : "plastic",
"sku" : 264221,
"isNew" : true
}
""".data(using: .utf8)!
let telephoneWithCord = try! JSONDecoder().decode(TelephoneWithCord.self, from: json)
telephoneWithCord.category
telephoneWithCord.material
telephoneWithCord.sku
telephoneWithCord.isNew
"Errors thrown", could perhaps, be a hint on how to fix this. Add throws to required init. Also, don't forget to call super for your code to be properly initialized or you will get another error. Try these changes ...
required init(from decoder: Decoder) throws { // add throws to eliminate errors
let container = try decoder.container(keyedBy: CodingKeys.self)
self.sku = try container.decode(Double.self, forKey: .sku)
self.isNew = try container.decode(Bool.self, forKey: .isNew)
try super.init(from: decoder) // calling super for proper intialization of code
}
As a side note: If you are not using any decimal points in your sku's, then you should change the type to Int instead of Double.
I'm using Swift decodable protocol to parse my JSON response:
{
"ScanCode":"4122001131",
"Name":"PINK",
"attributes":{
"type":"Product",
"url":""
},
"ScanId":"0000000kfbdMA"
}
I'm running into an issue where sometimes I get the ScanId value with a key "Id" instead of "ScanId".
Is there a way to work around that?
Thanks
You have to write a custom initializer to handle the cases, for example
struct Thing : Decodable {
let scanCode, name, scanId : String
private enum CodingKeys: String, CodingKey { case scanCode = "ScanCode", name = "Name", ScanID, Id }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
scanCode = try container.decode(String.self, forKey: .scanCode)
name = try container.decode(String.self, forKey: .name)
if let id = try container.decodeIfPresent(String.self, forKey: .Id) {
scanId = id
} else {
scanId = try container.decode(String.self, forKey: .ScanID)
}
}
}
First try to decode one key, if it fails decode the other.
For convenience I skipped the attributes key