Converting API JSON data to a Swift struct - json

I am using Swift for the first time and I'd like to be able to process some info from an API response into a usable Swift object.
I have (for example) the following data coming back from my API:
{
data: [{
id: 1,
name: "Fred",
info: {
faveColor: "red",
faveShow: "Game of Thrones",
faveIceCream: "Chocolate",
faveSport: "Hockey",
},
age: "28",
location: "The Moon",
},{
...
}]
}
In swift I have the data coming back from the API. I get the first object and I'm converting it and accessing it like so:
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let dataParentNode = json["data"] as! [[String:Any]]
let firstObject = dataParentNode[0]
let _id = firstObject["id"] as? String ?? "0"
let _name = firstObject["name"] as? String ?? "Unknown"
This is fine until I want to start processing the sub-objects belonging to the first object so I came up with the following structs to try and make this cleaner.
Please note - I don't need to process all of the JSON data coming back so I want to convert it to what I need in the structs
struct PersonInfo : Codable {
let faveColor: String?
let faveShow: String?
}
struct Person : Codable {
let id: String?
let name: String?
let info: PersonInfo?
}
When I take this:
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let dataParentNode = json["data"] as! [[String:Any]]
let firstObject = dataParentNode[0]
and then try to convert firstObject to Person or firstObject["info"] to PersonInfo I can't seem to get it to work (I get nil).
let personInfo = firstObject["info"] as? PersonInfo
Can anyone advise please? I just need to get my head around taking API response data and mapping it to a given struct (with sub-objects) ignoring the keys I don't need.

You can simply use decode(_:from:) function of JSONDecoder for this:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([String: [Person]].self, from: data)
let firstObject = decoded["data"]?.first
} catch {
print(error)
}
Even better you can add another struct to you model like this:
struct PersonsData: Codable {
let data: [Person]
}
And map your JSON using that type:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(PersonsData.self, from: data)
let firstObject = decoded.data.first
} catch {
print(error)
}
Update: Your Person struct might need a little change, because the id property is integer in your JSON.
So, it will end up like this:
struct Person : Codable {
let id: Int?
let name: String?
let info: PersonInfo?
}

Related

Generic decoding json into object with optionals

I wrote an extension to Decodable with the hopes of having a generic constructor for objects from json strings, it looks like this:
extension Decodable {
init?(with dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(
withJSONObject: dictionary,
options: .prettyPrinted
) else {
return nil
}
guard let result = try? JSONDecoder().decode(
Self.self,
from: data
) else {
return nil
}
self = result
}
}
An example use case looks like this:
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return }
guard var goer = Goer(with: json) else { return }
And the object I'm trying to decode into looks like this:
struct Goer: Codable {
let goerId: String
let created: Double
let username: String
let firstName: String
let lastName: String
let city: String
let bio: String
let isPrivate: Bool
let numFollowers: Int
let numFollowing: Int
let followers: [GoerFollow]
let following: [GoerFollow]
}
My issue is that I want to introduce some optionals to these objects that the json strings I'm trying to decode may or may not have keys for. This generic constructor fails in the case where there is no key:value in the json for an optional variable in the object.
I've seen that I can write a custom constructor with a decoder for each object and use the decodeIfPresent function but I wonder if there is a way to do it generically.
if let jsonData = data {
do {
var model = try decoder.decode(Goer.self, from: jsonData)
print("model:\(model)")
} catch {
print("error:\(error)")
}
}
struct Goer: Codable {
let goerId: String?
let created: Double?
let username: String?
let firstName: String?
let lastName: String?
let city: String?
let bio: String?
let isPrivate: Bool?
let numFollowers: Int?
let numFollowing: Int?
let followers: [GoerFollow]?
let following: [GoerFollow]?
}

Decode a nested object in Swift 5 with custom initializer

I have an API which returns a payload like this (just one item is included in the example).
{
"length": 1,
"maxPageLimit": 2500,
"totalRecords": 1,
"data": [
{
"date": "2021-05-28",
"peopleCount": 412
}
]
}
I know I can actually create a struct like
struct Root: Decodable {
let data: [DailyCount]
}
struct DailyCount: Decodable {
let date: String
let peopleCount: Int
}
For different calls, the same API returns the same format for the root, but the data is then different. Moreover, I do not need the root info (length, totalRecords, maxPageLimit).
So, I am considering to create a custom init in struct DailyCount so that I can use it in my URL session
let reports = try! JSONDecoder().decode([DailyCount].self, from: data!)
Using Swift 5 I tried this:
struct DailyCount: Decodable {
let date: String
let peopleCount: Int
}
extension DailyCount {
enum CodingKeys: String, CodingKey {
case data
enum DailyCountCodingKeys: String, CodingKey {
case date
case peopleCount
}
}
init(from decoder: Decoder) throws {
// This should let me access the `data` container
let container = try decoder.container(keyedBy: CodingKeys.self
peopleCount = try container.decode(Int.self, forKey: . peopleCount)
date = try container.decode(String.self, forKey: .date)
}
}
Unfortunately, it does not work. I get two problems:
The struct seems not to conform anymore to the Decodable protocol
The CodingKeys does not contain the peopleCount (therefore returns an error)
This can’t work for multiple reasons. You are trying to decode an array, so your custom decoding implementation from DailyCount won’t be called at all (if it were to compile) since at the top level your JSON contains an object, not an array.
But there is a much simpler solution which doesn’t even require implementing Decodable yourself.
You can create a generic wrapper struct for your outer object and use that with whatever payload type you need:
struct Wrapper<Payload: Decodable>: Decodable {
var data: Payload
}
You then can use this to decode your array of DailyCount structs:
let reports = try JSONDecoder().decode(Wrapper<[DailyCount]>.self, from: data).data
This can be made even more transparent by creating an extension on JSONDecoder:
extension JSONDecoder {
func decode<T: Decodable>(payload: T.Type, from data: Data) throws -> T {
try decode(Wrapper<T>.self, from: data).data
}
}
Sven's answer is pure and elegant, but I would be remiss if I didn't point out that there is also a stupid but easy way: dumpster-dive into the "data" without using Codable at all. Example:
// preconditions
let json = """
{
"length": 1,
"maxPageLimit": 2500,
"totalRecords": 1,
"data": [
{
"date": "2021-05-28",
"peopleCount": 412
}
]
}
"""
let jsonData = json.data(using: .utf8)!
struct DailyCount: Decodable {
let date: String
let peopleCount: Int
}
// okay, here we go
do {
let dict = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [AnyHashable:Any]
let arr = dict?["data"] as? Array<Any>
let json2 = try JSONSerialization.data(withJSONObject: arr as Any, options: [])
let output = try JSONDecoder().decode([DailyCount].self, from: json2)
print(output) // yep, it's an Array of DailyCount
} catch {
print(error)
}

Xcode - Parse string into usable data structure

I have a string (snippet below) that I know is valid json by parsing it here. The final parsing that I want to achieve is based off this PHP code. I can not seem to keep xcode happy when I try the following code, as I seem to continue to make array's of <Any> and either get the error Value of type 'Any' has no subscripts or an unwrapping of a nil
any thoughts how to parse this would be greatly appreciated.
if let jsonArray = try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!,options : .allowFragments) as? Array<Any> {
let a = jsonArray[0] as? Array<Any>
let a = jsonArray[0] as? Array<Any>
let b = a?[1] as? Array<Any>
let c = b?[1] as? Array<Array<Any>>
}
Snippet - full string here
[["comic books",[["","","","","","","","","Ba6eX5HWBc3btAa6mJ_4Cg","0ahUKEwjR7NSWsOHsAhXNLc0KHTrMB68QmBkIAigA",["comic books",0]
]
,["","","","","","","","","Ba6eX5HWBc3btAa6mJ_4Cg","0ahUKEwjR7NSWsOHsAhXNLc0KHTrMB68QmBkIBCgB","","","","",["Ba6eX5HWBc3btAa6mJ_4Cg","0ahUKEwjR7NSWsOHsAhXNLc0KHTrMB68Q8BcIBSgAMAA",["330 Army Trail Rd","Glendale Heights, IL 60139"]
,"",["","","",["https://www.google.com/search?q\u003dGot+Comics+Inc,+330+Army+Trail+Rd,+Glendale+Heights,+IL+60139\u0026ludocid\u003d7330282631276370350#lrd\u003d0x880fac8c25bbd32b:0x65ba6537347199ae,1","105
First, let's declare the data type:
struct Item {
let name: String?
let location: String?
let phone: String?
}
Using SwiftyJSON, you can nicely convert the PHP code into something like this:
var items = [Item]()
if let json = try? JSON(data: jsonData), let arr = json[0][1].array {
for itemArray in arr {
guard let itemDetails = itemArray[14].array else { continue }
let name = itemDetails[11].string
let location = itemDetails[18].string
let phone = itemDetails[178][0][0].string
items.append(Item(name: name, location: location, phone: phone))
}
}
For fun, here's a shorter, more functional, less readable, implementation of the above code:
let items = (try? JSON(data: jsonData))?[0][1].array?
.compactMap { $0[14].array }
.map { Item(name: $0[11].string, location: $0[18].string, phone: $0[178][0][0].string) }
?? []

Swift: Check if data response is an array or a dictonary

I'm calling an API and storing the data in an array.
But if there is no data the debugger says:
Expected to decode Array but found a dictionary instead.
The fail JSON response is:
'{"status":"Failed","msg":"Sorry It\'s Not Working"}'
The success JSON response is:
'[{"id":"509","name":"TEC TEST !"#!12","sortingId":"1"},
{"id":"510","name":"TEC TEST !"#!12","sortingId":"2"},
{"id":"511","name":"TEC TEST !"#!12","sortingId":"3"},
{"id":"512","name":"TEC TEST !"#!12","sortingId":"4"},
{"id":"513","name":"TEC TEST !"#!12","sortingId":"5"},
{"id":"514","name":"TEC TEST !"#!12","sortingId":"6"},
{"id":"519","name":"TEC TEST !"#!12","sortingId":"7"}]'
So I want to switch between fetching my response as
var result:[Items]?
and
var result:Items?
if the Failed JSON gets send
I've been google´ing and searching Stackoverflow without luck
Is there an solution to say if JSON is an array or dictionary?
My Struct:
struct Items: Codable {
let id: String?
let sortingId: String?
let name: String?
let response: String?
let status: String?
let msg: String?
}
My processing of the response:
var result:[Items]?
result = try JSONDecoder().decode([Items].self, from: data!)
DispatchQueue.main.async {
for item in result! {
self.itemArray.append((name: item.name!, id: Int(item.id!)!, sortingId: Int(item.sortingId!)!))
}
}
One solution is to write a custom initializer which conditionally decodes the array or the dictionary.
Please ask the owner of the service to send more consistent JSON. It's very bad. At least the object should be always a dictionary with key status and either the array for key items or the key msg.
This code first tries to decode the array with unkeyedContainer. If it fails it decodes the dictionary.
struct Item: Decodable {
let id: String
let sortingId: String
let name: String
}
struct ItemData : Decodable {
private enum CodingKeys: String, CodingKey { case status, msg }
let status : String?
let msg: String?
var items = [Item]()
init(from decoder: Decoder) throws {
do {
var unkeyedContainer = try decoder.unkeyedContainer()
while !unkeyedContainer.isAtEnd {
items.append(try unkeyedContainer.decode(Item.self))
}
status = nil; msg = nil
} catch DecodingError.typeMismatch {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decodeIfPresent(String.self, forKey: .status)
msg = try container.decodeIfPresent(String.self, forKey: .msg)
}
}
}
And call it
result = try JSONDecoder().decode(ItemData.self, from: data!)
A – probably more suitable – alternative is to catch the error in the JSONDecoder().decode line and use two simple structs
struct Item: Decodable {
let id: String
let sortingId: String
let name: String
}
struct ErrorData : Decodable {
let status : String
let msg: String
}
and call it
do {
let decoder = JSONDecoder()
do {
let result = try decoder.decode([Item].self, from: data!)
print(result)
} catch DecodingError.typeMismatch {
let result = try decoder.decode(ErrorData.self, from: data!)
print(result)
}
} catch { print(error) }
A big benefit is that all properties can be non-optional.
First of all the JSON does not contain any array. It's very very easy to read JSON. There are only 2 (two!) collection types, array [] and dictionary {}. As you can see there are no square brackets at all in the JSON string.
it will helpful

Convenient way to decode nested JSON object in Swift?

Let's say you have some JSON:
{
"status": "error",
"data": {
"errormessage": "Could not get user with ID: -1.",
"errorcode": 14
}
}
For a given Error struct:
struct APIError: Decodable {
let code: Int?
let message: String?
enum CodingKeys: String, CodingKey {
case code = "errorcode"
case message = "errormessage"
}
}
Hit the web service, get the JSON, and initialize the struct:
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest)
{ (data, response, error) in
// Doesn't work because the portion of the JSON we want is in the "data" key
let e = try? JSONDecoder().decode(APIError.self, from: data)
}
task.resume()
Is there some easy way to do something like data["data"]? What's the correct model to follow?
Solution A - Convert the data to a JSON object, get the object we want, then convert it to a Data object and decode.
let jsonFull = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
let json = jsonFull["data"]
let data_error = try? JSONSerialization.data(withJSONObject: json, options: [])
let e = try? JSONDecoder().decode(APIError.self, from: data_error)
Solution B - Wrap the target item in another struct
struct temp : Decodable {
let status: String?
let data: APIError?
}
let e = try? JSONDecoder().decode(temp.self, from: data).data
Solution C - Set the nested structure in decode (what if it is several objects deep?)
let e = try? JSONDecoder().decode([Any, APIError.self], from: data)
What patterns am I missing? What's the most elegant way to do this?
You can use the following approach:
struct APIError: Decodable {
let code: Int
let message: String
enum CodingKeys: String, CodingKey {
case data
}
enum ErrorCodingKeys: String, CodingKey {
case code = "errorcode"
case message = "errormessage"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let nestedContainer = try container.nestedContainer(keyedBy: ErrorCodingKeys.self, forKey: .data)
code = try nestedContainer.decode(Int.self, forKey: .code)
message = try nestedContainer.decode(String.self, forKey: .message)
}
}
let data = try! JSONSerialization.data(withJSONObject: ["status": "error", "data": ["errorcode": 14, "errormessage": "Could not get user with ID: -1."]], options: [])
let error = try! JSONDecoder().decode(APIError.self, from: data)