decoding JSON array or dictionary error swift - json

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.

Related

Decoding JSON that has a dynamic key value with the key value name found elsewhere in JSON model using Swift Decodable

When I make an API request, I get returned a JSON structure that contains several objects which all have unique key value names. Therefore I cannot use the regular Decodable protocol to break down the JSON into different instances. However, these key value names are found elsewhere in my JSON structure under known constant values that I can access normally. Here is some example code I created to demonstrate my issue:
let jsonString = """
{
"uniqueObjects": {
"uniqueObject:1132435": {
"firstName": "John"
"lastName": "Smith"
}
"uniqueObject2:119672": {
"firstName": "Jane"
"lastName": "Doe"
}
"uniqueObject3:008997": {
"firstName": "Sam"
"lastName": "Greenfield"
}
}
"keys": {
"object1": {
"key": "uniqueObject1:132435"
}
"object2": {
"key": "uniqueObject2:119672"
}
"object3": {
"key": "uniqueObject3:008997"
}
}
"""
let jsonData = Data(jsonString.utf8)
let decodedData = try? JSONDecoder().decode(JSONData.self, from: jsonData)
print(decodedData?.uniqueObjects.firstObject.firstName ?? "No data decoded")
struct JSONData: Decodable {
let uniqueObjects: Object
let keys: KeyObjects
}
struct Object: Decodable {
let firstObject: Names
let secondObject: Names
let thirdObject: Names
private enum DynamicCodingKeys: String, CodingKey {
case firstObject = "???" // this needs to be mapped to the unique value for object 1
case secondObject = "??" // this needs to be mapped to the unique value for object 2
case thirdObject = "????" // etc.
// I don't think this works because Xcode says that the strings must be raw literals
}
}
struct KeyObjects: Decodable {
let object1: Key
let object2: Key
let object3: Key
}
struct Key: Decodable {
let key: String
}
struct Names: Decodable {
let firstName: String
let lastName: String
}
The approach I took here, which is definitely wrong, was to create a coding key for each of the unique objects and map its name to a String that would be somehow be decoded from its relative key value pair in the key object. CodingKeys, at least what I have tried, do not allow you to do this so I need a new method in order to access this code. I also need to know how I can reference the data once I decode it (just printing it out for now). Help and a short explanation would be much appreciated as I am a beginner developer. Thanks!
Unless I have misunderstood it looks to me like you are over complicating things. This is how I would define the types for decoding the json
struct Response: Codable {
let uniqueObjects: [String: User]
let keys: [String: ObjectKey]
}
struct ObjectKey: Codable {
let key: String
}
struct User: Codable {
let firstName: String
let lastName: String
}

Swift JSON parse with colon in JSON key name

Can you use Swift's JSONDecoder on JSON that has a colon in the key name? Colons are reserved in Swift for things like indicating Type and Protocol, so when I try to create a struct with a matching keyname to use with JSONDecoder.decode, I'll get an error. Example API:
https://api.teleport.org/api/urban_areas/
the list of cities I'd like to access is in the key:
"ua:item"
But Swift doesn't allow a property with this name for easy JSONDecoding.
I'd like to stick with JSONDecoder since it's so easy & elegant. Is there an easy workaround for this, or do I need to fall back on older parsing techniques. Thanks!
Here is a reduced version of the JSON response for the API that you are calling. I have just limited the number of items in each array as it becomes to repetitive to just list similar items over and over again.
let data = """
{
"_links": {
"curies": [
{
"href": "https://developers.teleport.org/api/resources/Location/#!/relations/{rel}/",
"name": "location",
"templated": true
}
],
"self": {
"href": "https://api.teleport.org/api/urban_areas/"
},
"ua:item": [
{
"href": "https://api.teleport.org/api/urban_areas/slug:aarhus/",
"name": "Aarhus"
},
{
"href": "https://api.teleport.org/api/urban_areas/slug:adelaide/",
"name": "Adelaide"
}
]
},
"count": 266
}
""".data(using: .utf8)!
There are are couple of gotchas in the JSON, but we can fix them easily by using CodingKeys. Notice the case value of the CodingKey must match the variable name, the string value of the coding key must match the value in the JSON, where the values are the same we can skip writing the string value.
There are three issues with the JSON
_links
self
ua:item
By convention variables in Swift do not usually start with an underscore, so it makes sense to remove that. We can do that with the first set of CodingKeys.
self is a reserved word in Swift so we should replace that with something more appropriate, in this case I chose link.
As you have already noticed you cannot have colons in the name of variables. We can replace this with something more suitable, in this case uaItem.
This gives the following struct. Which if you take with the above data variable and past into a playground it should all decode nicely.
struct Response: Decodable {
let links: Links
let count: Int
// These are the coding keys for Response
enum CodingKeys: String, CodingKey {
case links = "_links"
case count
}
struct Links: Decodable {
let curies: [Curie]
let link: HREF
let uaItem: [UAItem]
// These are the coding keys for Links
enum CodingKeys: String, CodingKey {
case curies
case link = "self"
case uaItem = "ua:item"
}
}
struct Curie: Decodable {
let href: String
let name: String
let templated: Bool
}
struct HREF: Decodable {
let href: String
}
struct UAItem: Decodable {
let href: String
let name: String
}
}
do {
let result = try JSONDecoder().decode(Response.self, from: data)
print(result)
} catch {
print(error)
}

Swift: Parsing a JSON file where you don't know the key values

I'm building an app that queries wikidata using json. It works so far, but the issue I'm running into is the response I get from wikidata isn't the actual data, but an identifier. Then, to convert that identifier, I need to send it to another url and receive another json response, but the issue I'm running into is the the key value isn't something I know until I get the first json response in. So, lets say my key is Q1169621. When I run it through the api, I get this response:
I'm using codable and JSONDecoder, but I don't know how to tell the decoder the value of the key in entities is Q1169621 to get at the value I want ("Jim Lauderdale")...some of my code is below, I have structs to define the data of the response, but how do I replace the key value in my struct with the one defined from the previous json that is decoded?
struct InfoFromWikiConverted: Codable {
let entities: Locator //this is the value I need to set before parsing the json
}
struct Locator: Codable {
let labels: Labels //how do I link this to the struct above?
}
struct Labels: Codable {
let en: EN
}
struct EN: Codable {
let value: String
}
The simplest approach would be to decode entities as [String: Locator]:
struct InfoFromWikiConverted: Decodable {
let entities: [String: Locator]
}
Of course, if you want your model to just be a single Locator (which means, potentially ignoring multiple keys under entities), then you'd need to manually decode it.
You'd need to create a type to represent any string Coding Key and implement init(from:):
struct InfoFromWikiConverted: Decodable {
let entities: Locator
struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int? = nil
init(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// get the first key (ignore the rest), and throw if there are none
guard let key = container.allKeys.first else {
throw DecodingError.dataCorrupted(
.init(codingPath: container.codingPath,
debugDescription: "Expected a non-empty object"))
}
let entities = try container.decode(Locator.self, forKey: key)
}
}
Note that because you aren't retaining the ID, you can't encode it back to the same form, so I only conformed to Decodable instead of Codable

Is there a way to only partially create an object from JSON in Swift?

I'm creating a SwiftUI flashcard app, and I have no problem using Codable and following the technique Apple demonstrated with their landmarks tutorial app for importing JSON data in order to create their array of objects.
However, two of my flashcard objects' properties don't need to be loaded from JSON, and I could minimize the text needed in the JSON file if I could initialize those values separately instead of loading them from JSON. The problem is I cannot get JSON data to load without an error unless it maps exactly to ALL the object's properties, even if the missing properties are hardcoded with values.
Here is my object model:
import SwiftUI
class Flashcard: Codable, Identifiable {
let id: Int
var status: Int
let chapter: Int
let question: String
let answer: String
let reference: String
}
Here is JSON that works:
[
{
"id": 1,
"status": 0,
"chapter": 1,
"question": "This is the question",
"answer": "This is the answer",
"reference": "This is the reference"
}
//other card info repeated after with a comma separating each
]
Instead of having "id" and "status" listed unecessarily in the JSON, I would prefer to change the model to something like this:
import SwiftUI
class Flashcard: Codable, Identifiable {
let id = UUID()
var status: Int = 0
//only load these from JSON:
let chapter: Int
let question: String
let answer: String
let reference: String
}
...and then I theoretically should be able to eliminate "id" and "status" from the JSON (but I can't). Is there a simple way to do this that prevents the error from JSON not mapping completely to the object?
Yes, you can do this by setting the coding keys on your Codable class. Just leave out the ones that you don't want to update from the json.
class Flashcard: Codable, Identifiable {
let id = UUID()
var status: Int = 0
let chapter: Int
let question: String
let answer: String
let reference: String
enum CodingKeys: String, CodingKey {
case chapter, question, answer, reference
}
}
There is a great article by HackingWithSwift on Codable here
you can use CodingKeys to define what fields to extract from the JSON.
class Flashcard: Codable, Identifiable {
enum CodingKeys: CodingKey {
case chapter
case question
case answer
case reference
}
let id = UUID()
var status: Int = 0
//only load these from JSON:
let chapter: Int
let question: String
let answer: String
let reference: String
}
The docuemntation has a good explanation (for once) of this under `Encoding and Decoding Custom Types`

Swift Codable decode changing JSON and ignoring some fields

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