Swift Codable:Nested Dictionary not being read - swift

I am getting some json like so (this is pseudo, not all keys are here):
{
"absolute_magnitude_h" = "23.4";
...
"close_approach_data" = (
{
"close_approach_date" = "1994-09-03";
"epoch_date_close_approach" = 778575600000;
"orbiting_body" = Earth;
}
I have an object with this struct:
struct NEOObj:Codable {
var absoluteMagnitudeH:Float
var designation:String
var isPotentiallyHazardousAsteroid:Bool
var isSentryObject:Bool
var name:String
var nasaJPLURL:String
var neoReferenceID:String
var closeApproachData:[NEOCloseApproachData] = [NEOCloseApproachData]()
private enum CodingKeys: String, CodingKey {
case absoluteMagnitudeH = "absolute_magnitude_h"
case designation = "designation"
case isPotentiallyHazardousAsteroid = "is_potentially_hazardous_asteroid"
case isSentryObject = "is_sentry_object"
case name = "name"
case nasaJPLURL = "nasa_jpl_url"
case neoReferenceID = "neo_reference_id"
}
enum CloseApproachCodingKeys: String, CodingKey {
case closeApproachDate = "close_approach_date"
case epochDateCloseApproach = "epoch_date_close_approach"
case orbitingBody = "orbiting_body"
}
struct NEOCloseApproachData:Codable {
var closeApproachDate:Date
var epochDateCloseApproach:Date
var orbitingBody:String
enum CodingKeys: String, CodingKey {
case closeApproachDate = "close_approach_date"
case epochDateCloseApproach = "epoch_date_close_approach"
case orbitingBody = "orbiting_body"
}
}
and in my file I have this code:
if let arrNEOs = dictJSON["near_earth_objects"] as? Array<Any> {
for thisNEODict in arrNEOs {
do {
let jsonData = try JSONSerialization.data(withJSONObject: thisNEODict, options: .prettyPrinted)
let thisNEOObj = try? JSONDecoder().decode(NEOObj.self, from: jsonData)
print(thisNEOObj!.closeApproachData)
} catch {
}
}
}
But closeApproachData never gets populated. What am I doing wrong?

There are multiple things that would need to be changed for it to work.
closeApproachData should probably be of type [NEOCloseApproachData]. You haven't included CloseApproachCodingKeys in your code but it probably isn't right.
NEOObj.CodingKeys needs to have a case for closeApproachData which would look like
case closeApproachData = "close_approach_data"
NEOCloseApproachData needs a custom decoder to work with the dates for closeApproachDate and epochDateCloseApproach. Since the JSON has different formats for each (String and Int respectively), you can't use on the JSONDecoder.dateDecodingStrategy since it will apply to all dates.

The reason why after decoding you get closeApproachData property empty is the lack of the regarding key for that property into your CodingKeys enum.
Regarding to the Apple doc, it tells next:
Omit properties from the CodingKeys enumeration if they won't be
present when decoding instances, or if certain properties shouldn't be
included in an encoded representation.
That means that generated implementation of decode method for your NEOObj class omit decoding implementation for the closeApproachData property.
Here's a link at the Apple documentation about Encoding and Decoding Custom Types. You can find more details at the "Choose Properties to Encode and Decode Using Coding Keys" paragraph.
Also your custom class NEOCloseApproachData should be conformed to the Coding protocol.

Related

Swift 5 JSON Import with CodingKeys and ENUM values

Reading in from small JSON file (cut/pasted small sample) and bring it into structs. I am using CodingKeys to change the "string key" to match what I want in the struct. I am changing the "whoresponds" key to "respondingPilot". That is all working fine. However, I also want to look at the values on that property and change on-the-fly the value brought in. For example, if I get the string "BOTH", I want to change my data that is stored to "CAPT & F/O". Same if I see "FO", I want that changed to "F/O" as it read in. Sample below reads in fine but will not make the changes. Still learning but must be something simple I am missing. Thanks!
struct CheckListsJSON: Codable {
var name: String
var checklistItems: [ChecklistItemsJSON]
}
struct ChecklistItemsJSON: Codable, Identifiable {
var challenge: String
var respondingPilot: PilotResponding
let id = UUID()
private enum CodingKeys: String, CodingKey {
case challenge
case respondingPilot = "whoresponds"
}
enum PilotResponding: String, Codable {
case CPT = "CAPT"
case FO = "F/O"
case PF = "PF"
case PM = "PM"
case BOTH = "CAPT & F/O"
}
}
let jsonAC = "{\"name\": \"After Start\", \"checklistItems\": [{\"challenge\": \"Generators\", \"whoresponds\": \"CPT\"}, {\"challenge\": \"Isolation Valve\", \"whoresponds\": \"FO\"}}"
let fileData = Data(jsonAC.utf8)
do {
let decodedData = try JSONDecoder().decode(CheckListsJSON.self, from: fileData)
print("decoded:", decodedData)
} catch {
print(error)
}
Decoding a string to an enum works like CodingKeys. The raw value is the string you receive and the case is the case.
Unfortunately the character set of enum cases is restricted. Space characters, / and & are not allowed.
You could write something like this, raw values which match the case can be omitted.
enum PilotResponding: String, Codable {
case CAPT = "CPT"
case F_O = "FO"
case PF, PM
case CAPT_F_O = "BOTH"
}

Cannot iterate through array inside array of dictionaries

I'm having trouble parsing a response into workable objects in swift.
Basically, this is the response I get (simplified data for viewing purposes)
"[{\"CO2\":0,\"Places\":[{\"Name\":\"RT(Esc. Sec.)\",\"Code\":\"ST_RT\",\"Type\":0,\"CoordX\":41.176750183105469,\"CoordY\":-8.5490522384643555,\"Provider\":\"ST\",\"Lines\":null},{\"Name\":\"Esc.Sec RT\",\"Code\":\"ST_RT2\",\"Type\":0,\"CoordX\":41.175251007080078,\"CoordY\":-8.54929256439209,\"Provider\":\"ST\",\"Lines\":null},{\"Name\":\"SM\",\"Code\":\"ST_SM\",\"Type\":0,\"CoordX\":41.173740386962891,\"CoordY\":-8.5474367141723633,\"Provider\":\"ST\",\"Lines\":null}],\"Direction\":\"R\"}]"
After I receive the response I do the following:
let dict = try! JSONSerialization.jsonObject(data: responseData!, options: .allowFragments) as? [[String:Any]] ?? [[String:Any]]()
Which results in the following dictionary (I'm sorry for the picture, but I could not take a print screen. Plus those 44 elements are the total number of Places, but I've simplified the response string as I said above)
My problem is, I cannot access each Place dictionary. I've tried iterating through dict["Places"] but that does not seem to work, which I do not understand, given it is an NSArray.
I may be missing something simple, but I can't seem to figure it out.
Any help is welcome.
Thanks!
You can try
// MARK: - Root
struct Root: Codable {
let co2: Int
let places: [Place]
let direction: String
enum CodingKeys: String, CodingKey {
case co2 = "CO2"
case places = "Places"
case direction = "Direction"
}
}
// MARK: - Place
struct Place: Codable {
let name, code: String
let type: Int
let coordX, coordY: Double
let provider: String
let lines: String?
enum CodingKeys: String, CodingKey {
case name = "Name"
case code = "Code"
case type = "Type"
case coordX = "CoordX"
case coordY = "CoordY"
case provider = "Provider"
case lines = "Lines"
}
}
let res = try JSONDecoder().decode([Root].self,from:data)
print(res.places)

Assign dynamically properties of a Struct in Swift

I have this Struct:
struct Alphabet {
let a = "ciao"
let b = "hi"
let c = "hola"
}
let alphabet = Alphabet()
I want the value of each property become the string of the property itself.
Like this:
alphabet.a = "a"
alphabet.b = "b"
alphabet.c = "c"
But I want to be done regardless of the number of properties or their value:
I tried this:
Mirror(reflecting: Alphabet.self).children.forEach { (label, value) in
self.alphabet[keyPath: label] = label!
}
but I know this is not the way KeyPath works...
Probably there are type safety problems going on too.
Any idea?
As far as I know keyPaths aren't the way to go, you need to use CodingKeys
Here's a working sample, creating JSON and then decoding it might not be the perfect so you better change my solution to suit your needs.
struct Alphabet: Codable {
let a: String
let b: String
let c: String
enum CodingKeys: String, CodingKey, CaseIterable
{
case a
case b
case c
}
static func generateJSON() -> String {
var json = "{"
for i in CodingKeys.allCases
{
json += "\"\(i.stringValue)\": \"\(i.stringValue)\","
}
json.removeLast()
json += "}"
return json
}
}
let decoder = JSONDecoder()
let alphabet = try decoder.decode(Alphabet.self, from: Alphabet.generateJSON().data(using: .utf8)!)
print(alphabet.a) //Prints "a"

Decoding numerical snake_case keys with JSONDecoder

I have the following JSON object which is to be converted to an object using JSONDecoder:
{
"first_key": 3,
"image_1000x1000": "location"
}
This maps to the following Swift model:
struct Response: Decodable {
var firstKey: Int
var image1000x1000: String
}
By using JSONDecoder with the .convertFromSnakeCase option, the snake_case keys inside the JSON are transformed to camelCase by using the algorithm defined in the documentation:
This strategy follows these steps to convert JSON keys to camel-case:
Capitalize each word that follows an underscore.
Remove all underscores that aren't at the very start or end of the string.
Combine the words into a single string.
Therefore, in this case:
first_key becomes firstKey (as expected)
image_1000x1000 should become image1000x1000
However when attempting to decode this response, a keyNotFound error for the image1000x1000 key is thrown (see this live example):
let json = "{\"first_key\": 3, \"image_1000x1000\": \"location\"}".data(using: .utf8)!
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode(Response.self, from: json)
print(response)
} catch let e {
print(e)
}
What is incorrect about my camel case conversion of image_1000x1000, and why can’t JSONDecoder find the corresponding key?
You can see what the algorithm is expecting by running the process in reverse; use a JSONEncoder to encoder your data and inspect the output:
struct Response: Codable {
var firstKey: Int
var image1000x1000: String
}
let test = Response(firstKey: 10, image1000x1000: "secondKey" )
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(test)
print(String(data: data, encoding: .utf8)!)
This will produce:
{"first_key":10,"image1000x1000":"secondKey"}
So if you have control over the JSON and can live with image1000x1000 as a key, then you're done. If not you'll have to do something like this:
struct Response: Codable {
var firstKey: Int
var image1000x1000: String
private enum CodingKeys: String, CodingKey {
case image1000x1000 = "image_1000x1000"
case firstKey = "first_key"
}
}
Another option is to implement a custom key encoding strategy. It may end up being less code. See KeyEncodingStrategy for more about this.
I am facing the same issue my json keys are iso639_1 and iso639_2. Without adding Coding key it will works.
make your variable optional
Here it's my Decodable model
struct CountryModel: Decodable{
var name: String
var borders: [String]
var region: String
var alpha2Code: String
var alpha3Code: String
var flag: String
var languages: [languages]
}
struct languages: Decodable {
var iso639_1: String?
var iso639_2: String?
var name: String
var nativeName: String
}
When I add optional to both underscore variables iso639_1 and iso639_2. Then it works fine may be due to Null Value!
Here, in your case add optional to your image1000x1000 variable. Like below
struct Response: Decodable {
var firstKey: Int
var image_1000x1000: String?
}
Hope it will works!

What does .decode return in swift 4?

When you call .decode() to decode a struct, what exactly does it return?
I have look it up on the Apple Documentation, but all it says is "native format into in-memory representations." But what does this mean? Can anyone help me?
I'm asking this because my app is crashing when I get a null value from the JSON data, from this line of code:
let plantData = try decoder.decode([Plants].self, from: data)
Here is my struct:
struct Plants: Codable {
let date: String
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And Here is my Parsing code:
func parseJson() {
let url = URL(string: ebr_String)
// Load the URL
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
// If there are any errors don't try to parse it, show the error
guard let data = data, error == nil else { print(error!); return }
let decoder = JSONDecoder()
do{
let plantData = try decoder.decode([Plants].self, from: data)
print(plantData)
And Here is just a snippet of the information I am getting back:
MorrowTabbedApp.Plants(date: "2018-02-22", monthlyAVG: Optional("1210.06")), MorrowTabbedApp.Plants(date: "2018-02-23", monthlyAVG: nil)]
Here is the snippet of JSON from the web:
[
{"Date":"2018-02-21","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-22","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-23","30_Day_MA_MMBTU":null}
]
The decode method of JSONDecoder is a "generic" method. It returns an instance of whatever type you specified in the first parameter of the method. In your case, it returns a [Plants], i.e. a Array<Plants>, i.e. a Swift array of Plants instances.
If it's crashing because of a null value in your JSON, then you have to identify what was null, whether it was appropriate to be null, and if so, make sure that any Plants properties associated with values that might be null should be optionals.
Given your updated answer with code snippets, I'd suggest:
// Personally, I'd call this `Plant` as it appears to represent a single instance
struct Plant: Codable {
let date: String
let monthlyAVG: String? // Or you can use `String!` if you don't want to manually unwrap this every time you use it
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And:
do {
let plantData = try JSONDecoder().decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
Note the filter line which selects only those occurrences for which monthlyAVG is not nil.
A couple of other suggestions:
Personally, if you could, I'd rather see the web service designed to only return the values you want (those with an actual monthlyAVG) and then change the monthlyAVG property to not be an optional. But that's up to you.
If monthlyAVG is really a numeric average, I'd change the web service to not return it as a string at all, but as a number without quotes. And then change the property of Plant to be Double or whatever.
You could, if you wanted, change the date property to be a Date and then use dateDecodingStrategy to convert the string to a Date:
struct Plant: Codable {
let date: Date
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
and
do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
let plantData = try decoder.decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
You might do this if, for example, you wanted the x-axis of your chart to actually represent time rather than an evenly spaced set of data points.