Nested JSON in SWIFT 5 - json

even though there are many articles about nested json decoding, I'm struggling with following one:
sample from json:
{
"00AK": {
"icao": "00AK",
"iata": "",
"name": "Lowell Field",
"city": "Anchor Point",
"state": "Alaska",
"country": "US",
"elevation": 450,
"lat": 59.94919968,
"lon": -151.695999146,
"tz": "America\/Anchorage"
},
"00AL": {
"icao": "00AL",
"iata": "",
"name": "Epps Airpark",
"city": "Harvest",
"state": "Alabama",
"country": "US",
"elevation": 820,
"lat": 34.8647994995,
"lon": -86.7703018188,
"tz": "America\/Chicago"
},
"00AZ": {
"icao": "00AZ",
"iata": "",
"name": "Cordes Airport",
"city": "Cordes",
"state": "Arizona",
"country": "US",
"elevation": 3810,
"lat": 34.3055992126,
"lon": -112.1650009155,
"tz": "America\/Phoenix"
},
....
}
I start with mapping the airport specifics with following struct:
struct Airport : Codable {
var icao:String
var iata:String
var name:String
var city:String
var state:String
var country:String
var elevation: Double
var lat: Double
var lon: Double
var tz: Double
enum CodingKeys : String, CodingKey {
case icao
case iata
case name
case city
case state
case country
case elevation
case lat
case lon
case tz
}
}
but I'm stuck - unable to find out how to map a "mother" struct of airport code (00AK, 00AL, 00AZ) and how to look up in decoded data parsed here:
let decodedData = try JSONDecoder().decode(AirportsStructToBeCreated.self,
from: jsonData)
(e.g. how to look up "elevation" of "00AZ")

You have to decode a [String:Airport] dictionary
let decodedData = try JSONDecoder().decode([String:Airport].self,
from: jsonData)
or you could implement init(from decoder and map the dictionaries to an array as the key is also included in the struct.
And you can omit the CodingKeys if the struct members match exactly the dictionary keys.
And tz is not Double

Related

How to parse this type of data to a JSON in Swift?

I have called an API to get all holidays in a year, it came out a Json type. But I only can extract it like below (it is one of many elements of "items")
"items": [
{
"kind": "calendar#event",
"etag": "\"3235567993214000\"",
"id": "20200101_1814eggq09ims8ge9pine82pclgn49rj41262u9a00oe83po05002i01",
"status": "confirmed",
"htmlLink": "https://www.google.com/calendar/event?eid=MjAyMDAxMDFfMTgxNGVnZ3EwOWltczhnZTlwaW5lODJwY2xnbjQ5cmo0MTI2MnU5YTAwb2U4M3BvMDUwMDJpMDEgZW4udWsjaG9saWRheUB2",
"created": "2021-04-07T08:26:36.000Z",
"updated": "2021-04-07T08:26:36.607Z",
"summary": "New Year's Day",
"creator": {
"email": "en.uk#holiday#group.v.calendar.google.com",
"displayName": "Holidays in United Kingdom",
"self": true
},
"organizer": {
"email": "en.uk#holiday#group.v.calendar.google.com",
"displayName": "Holidays in United Kingdom",
"self": true
},
"start": {
"date": "2020-01-01"
},
"end": {
"date": "2020-01-02"
},
"transparency": "transparent",
"visibility": "public",
"iCalUID": "20200101_1814eggq09ims8ge9pine82pclgn49rj41262u9a00oe83po05002i01#google.com",
"sequence": 0,
"eventType": "default"
},
{
"kind": "calendar#event",
"etag": "\"3235567993214000\"",
"id": "20200412_1814eggq09ims8gd8lgn6t35e8g56tbechgniag063i0ue048064g0g",
"status": "confirmed",
"htmlLink": "https://www.google.com/calendar/event?eid=MjAyMDA0MTJfMTgxNGVnZ3EwOWltczhnZDhsZ242dDM1ZThnNTZ0YmVjaGduaWFnMDYzaTB1ZTA0ODA2NGcwZyBlbi51ayNob2xpZGF5QHY",
"created": "2021-04-07T08:26:36.000Z",
"updated": "2021-04-07T08:26:36.607Z",
"summary": "Easter Sunday",
"creator": {
"email": "en.uk#holiday#group.v.calendar.google.com",
"displayName": "Holidays in United Kingdom",
"self": true
},
"organizer": {
"email": "en.uk#holiday#group.v.calendar.google.com",
"displayName": "Holidays in United Kingdom",
"self": true
},
"start": {
"date": "2020-04-12"
},
"end": {
"date": "2020-04-13"
},
"transparency": "transparent",
"visibility": "public",
"iCalUID": "20200412_1814eggq09ims8gd8lgn6t35e8g56tbechgniag063i0ue048064g0g#google.com",
"sequence": 0,
"eventType": "default"
}
I try to get the value in key "start" and key "summary" but I can't.
Xcode told me that "items" is a __NSArrayI type.
What I've tried so far is create a class simple like this (just use to try first, so I didn't make all variable)
class API_Info {
var kind: String?
var etag: String?
var id: String?
var status: String?
var htmlLink: String?
var created: String?
var updated: String?
var summary: String?
init(items: [String:Any]){
self.kind = items["kind"] as? String
self.etag = items["etag"] as? String
self.id = items["id"] as? String
self.status = items["status"] as? String
self.htmlLink = items["htmlLink"] as? String
self.created = items["created"] as? String
self.updated = items["updated"] as? String
self.summary = items["summary"] as? String
}
}
And I parse like this:
guard let items = json!["items"]! as? [API_Info] else{
print("null")
return
}
In this way, else statement was run.
What am I doing wrong, and how can I get the data I want?
Thanks in advance.
Codable is the solution here. Below is the struct I used rather than your class
struct ApiInfo: Codable {
let kind: String
let etag: String
let id: String
let status: String
let htmlLink: String
let created: Date
let updated: Date
let summary: String
}
Then I created a root type to hold the array
struct Result: Codable {
let items: [ApiInfo]
}
And then the decoding
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
do {
let result = try decoder.decode(Result.self, from: data)
print(result.items)
} catch {
print(error)
}
Notice that the data values are decoded to Date objects.
Optionally you can skip the root type and decode as a dictionary
do {
let items = try decoder.decode([String: [ApiInfo]].self, from: data)
if let values = items["items"] {
print(values)
}
} catch {
print(error)
}

Issue with Codable and JSON

I'm trying to avoid use all the boilerplate code stuff without codable.
I apologize if this a dumb question, but why am I getting this error when I'm trying to parse json with codable?
keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))
The json endpoint is structured as follows:
{
"businesses": [
{
"id": "FmGF1B-Rpsjq1f5b56qMwg",
"alias": "molinari-delicatessen-san-francisco",
"name": "Molinari Delicatessen",
"image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/6He-NlZrAv2mDV-yg6jW3g/o.jpg",
"is_closed": false,
"url": "https://www.yelp.com/biz/molinari-delicatessen-san-francisco?adjust_creative=Js10QwuboHe9ZMZF31mwuw&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=Js10QwuboHe9ZMZF31mwuw",
"review_count": 1058,
"categories": [
{
"alias": "delis",
"title": "Delis"
}
],
"rating": 4.5,
"coordinates": {
"latitude": 37.79838,
"longitude": -122.40782
},
"transactions": [
"pickup",
"delivery"
],
"price": "$$",
"location": {
"address1": "373 Columbus Ave",
"address2": "",
"address3": "",
"city": "San Francisco",
"zip_code": "94133",
"country": "US",
"state": "CA",
"display_address": [
"373 Columbus Ave",
"San Francisco, CA 94133"
]
},
"phone": "+14154212337",
"display_phone": "(415) 421-2337",
"distance": 1453.998141679007
}
}
I have a struct I created
struct Businesses: Codable {
var name:String
var image_url:String
var is_closed:Bool
var location:[String:String]
var display_phone:String
var url:String
}
Here's how I'm trying to use Codable:
do{
let jsonData = response.data
//created the json decoder
let decoder = JSONDecoder()
//using the array to put values
self.searchResults = [try decoder.decode(Businesses.self, from: jsonData!)]
It's a very common mistake, you are ignoring the root object which – of course – does not have a key name.
And you'll get another error, location is not [String:String]
struct Root : Decodable {
let businesses : [Business]
}
// Name this kind of struct in singular form
struct Business : Decodable {
let name: String
let imageUrl: URL
let isClosed: Bool
let location: Location
let displayPhone: String
let url: URL
}
struct Location : Decodable {
let address1, address2, address3: String
let city, zipCode, country, state: String
let displayAddress: [String]
}
...
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let jsonData = response.data
let result = try decoder.decode(Root.self, from: jsonData!)
self.searchResults = result.businesses
} catch { print(error) }

How can I populate part of a JSON object without repetitive code?

I am making use of the Microsoft Graph API, specifically the FindMeetingTimes API. Which can be seen here: https://learn.microsoft.com/en-us/graph/api/user-findmeetingtimes?view=graph-rest-1.0
I am using Swift to develop this. My JSON object works and the post is successful, but I am just wondering what would be the best / most efficient way to alter this code so that it cuts out the repetitive code - specifically the adding of locations in the JSON. The locations list could be very large, so I would like to iterate through an locations array and add these to the JSON object..
My method looks like this:
static func setupJsonObjectForFindMeetingTimeAllRooms(nameOfRoom: String, roomEmailAddress: String, dateStartString: String, dateEndString: String, durationOfMeeting: String) -> [String: Any] {
let jsonObj : [String: Any] =
[
"attendees": [
[
"type": "required",
"emailAddress": [
"name": nameOfRoom,
"address": roomEmailAddress
]
]
],
"locationConstraint": [
"isRequired": "true",
"suggestLocation": "false",
"locations": [
[
"displayName": "First Floor Test Meeting Room 1",
"locationEmailAddress": "FirstFloorTestMeetingRoom1#microsoft.com"
],
[
"displayName": "Ground Floor Test Meeting Room 1",
"locationEmailAddress": "GroundFloorTestMeetingRoom1#microsoft.com"
]
//and the rest of the rooms below this.. how do i do this outside in a loop? to prevent repetitive code?
]
],
"timeConstraint": [
"activityDomain":"unrestricted",
"timeslots": [
[
"start": [
"dateTime": dateStartString,
"timeZone": Resources.utcString
],
"end": [
"dateTime": dateEndString,
"timeZone": Resources.utcString
]
]
]
],
"meetingDuration": durationOfMeeting,
"returnSuggestionReasons": "true",
"minimumAttendeePercentage": "100",
"isOrganizerOptional": "true"
]
return jsonObj
}
What is the best way to go about doing this? Would I just remove the locations part of the JSON, and before returning, populate it with the array of locations?
I tried to implement a method to add locations to the JSON - using this method:
static func addLocationsToExistingJson(locations: [String], jsonObj: [String: Any]) -> [String: Any] {
var data: [String: Any] = jsonObj
for i in stride(from: 0, to: locations.count, by: 1){
let item: [String: Any] = [
"displayName": locations[i],
"locationEmailAddress": locations[i]
]
// get existing items, or create new array if doesn't exist
var existingItems = data["locations"] as? [[String: Any]] ?? [[String: Any]]()
// append the item
existingItems.append(item)
// replace back into `data`
data["locations"] = existingItems
}
return data
}
and calling this method before returning the JSON in the original method.. But it seems that the final JSON is not the format that I want.
The incorrect version looks like this:
["timeConstraint": ["activityDomain": "unrestricted", "timeslots": [["start": ["dateTime": "2019-02-07 14:00:00", "timeZone": "UTC"], "end": ["dateTime": "2019-02-07 15:00:00", "timeZone": "UTC"]]]], "minimumAttendeePercentage": "100", "isOrganizerOptional": "true", "returnSuggestionReasons": "true", "meetingDuration": "PT60M", "attendees": [["type": "required", "emailAddress": ["name": "N", "address": "TestUser6#qubbook.onmicrosoft.com"]]], "locationConstraint": ["isRequired": "true", "suggestLocation": "false"]]
Whereas the working JSON looks like this:
["timeConstraint": ["activityDomain": "unrestricted", "timeslots": [["start": ["dateTime": "2019-02-07 14:30:00", "timeZone": "UTC"], "end": ["dateTime": "2019-02-07 15:30:00", "timeZone": "UTC"]]]], "attendees": [["type": "required", "emailAddress": ["name": "N", "address": "TestUser6#qubbook.onmicrosoft.com"]]], "minimumAttendeePercentage": "100", "locations": [["displayName": "FirstFloorTestMeetingRoom1#qubbook.onmicrosoft.com", "locationEmailAddress": "FirstFloorTestMeetingRoom1#qubbook.onmicrosoft.com"], ["displayName": "GroundFloorTestMeetingRoom1#qubbook.onmicrosoft.com", "locationEmailAddress": "GroundFloorTestMeetingRoom1#qubbook.onmicrosoft.com"]], "locationConstraint": ["isRequired": "true", "suggestLocation": "false"], "meetingDuration": "PT60M", "isOrganizerOptional": "true", "returnSuggestionReasons": "true"]
How would I change my code so that the locations get added under the locationConstraint object within the JSON, rather than just to the JSON, not under the ["locationConstraint"] part?
As Joakim suggested, you could model the json structure via structs and Codable. For example:
struct AdditionalData: Codable {
let emailAdress: [String]
}
struct Person: Codable {
let age: Int
let name: String
let additionalData: AdditionalData
}
let additionalData = AdditionalData(emailAdress: ["test#me.com", "foo#bar.com"])
let p1 = Person(age: 30,
name: "Frank",
additionalData: additionalData)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let data = try? encoder.encode(p1),
let jsonString = String(data: data, encoding: .utf8) {
print(jsonString)
}
Which results in:
{
"age" : 30,
"name" : "Frank",
"additionalData" : {
"emailAdress" : [
"test#me.com",
"foo#bar.com"
]
}
}

json array with same names

I am trying to parse through and record json data that has an array with multiple of the same title using swift4. I have included some of the raw json data below:
{
"league": {
"alias": "MLB",
"name": "Major League Baseball",
"id": "2fa448bc-fc17-4d3d-be03-e60e080fdc26",
"date": "2018-07-07",
"games": [
{
"game": {
"id": "d9c6012e-4328-4283-9d6e-b95ff3d53106",
"status": "scheduled",
"coverage": "full",
"game_number": 1,
"day_night": "N",
"scheduled": "2018-07-07T23:15:00+00:00",
"home_team": "833a51a9-0d84-410f-bd77-da08c3e5e26e",
"away_team":
},
"game": {
"id": "d9c6012e-4328-4283-9d6e-b95ff3d53106",
"status": "scheduled",
"coverage": "full",
"game_number": 1,
"day_night": "N",
"scheduled": "2018-07-07T23:15:00+00:00",
"home_team": "833a51a9-0d84-410f-bd77-da08c3e5e26e",
"away_team":
}
}
]
};
The following code are the structs to store the json data. I am getting an error on the leagueinfo line that says does not conform to decodable but I believe the issue is from the games struct. I cannot figure out how to bring in multiple items named "game":
struct League: Decodable{
let league: LeagueInfo
init(league: LeagueInfo ){
self.league = league
}
}
struct LeagueInfo: Decodable{
let alias: String?
let name: String?
let id: String?
let date: String?
let games: Games?
}
struct Games{
let game: [Game]
}
struct Game: Decodable{
let id: String?
let status: String?
let coverage: String?
let scheduled: String?
let home_team: String?
let away_team: String?
let venue: [Venue]?
}
Any help would be greatly apreciated!
I am looking at your "games" attribute. It seems your array is not json valid. I think your array is supposed to be like this.
json = {
"games": [
{
"game": {
"id": "d9c6012e-4328-4283-9d6e-b95ff3d53106",
"status": "scheduled",
"coverage": "full",
"game_number": 1,
"day_night": "N",
"scheduled": "2018-07-07T23:15:00+00:00",
"home_team": "833a51a9-0d84-410f-bd77-da08c3e5e26e",
"away_team": ""
}
},
{
"game": {
"id": "d9c6012e-4328-4283-9d6e-b95ff3d53106",
"status": "scheduled",
"coverage": "full",
"game_number": 1,
"day_night": "N",
"scheduled": "2018-07-07T23:15:00+00:00",
"home_team": "833a51a9-0d84-410f-bd77-da08c3e5e26e",
"away_team": ""
}
}
]
};
Notice your extra curly brackets and missing commas.

Error while iterating over dictionary json data with swift: Could not find an overload for 'subscript' that accepts the supplied arguments

I figured out how to read a local json file in my project. But now I am having trouble iterating over it. Here is my raw json data:
{
"locations": [
{
"title": "The Pump Room",
"place": "Bath",
"latitude": 51.38131,
"longitude": -2.35959,
"information": "The Pump Room Restaurant in Bath is one of the city’s most elegant places to enjoy stylish, Modern-British cuisine.",
"telephone": "+44 (0)1225 444477",
"url": "http://www.romanbaths.co.uk",
"visited" : true
},
{
"title": "The Eye",
"place": "London",
"latitude": 51.502866,
"longitude": -0.119483,
"information": "At 135m, the London Eye is the world’s largest cantilevered observation wheel. It was designed by Marks Barfield Architects and launched in 2000.",
"telephone": "+44 (0)8717 813000",
"url": "http://www.londoneye.com",
"visited" : false
},
{
"title": "Chalice Well",
"place": "Glastonbury",
"latitude": 51.143669,
"longitude": -2.706782,
"information": "Chalice Well is one of Britain's most ancient wells, nestling in the Vale of Avalon between the famous Glastonbury Tor and Chalice Hill.",
"telephone": "+44 (0)1458 831154",
"url": "http://www.chalicewell.org.uk",
"visited" : true
},
{
"title": "Tate Modern",
"place": "London",
"latitude": 51.507774,
"longitude": -0.099446,
"information": "Tate Modern is a modern art gallery located in London. It is Britain's national gallery of international modern art and forms part of the Tate group.",
"telephone": "+44 (0)20 7887 8888",
"url": "http://www.tate.org.uk",
"visited" : true
},
{
"title": "Eiffel Tower",
"place": "Paris",
"latitude": 48.858271,
"longitude": 2.294114,
"information": "The Eiffel Tower (French: La Tour Eiffel, is an iron lattice tower located on the Champ de Mars in Paris.",
"telephone": "+33 892 70 12 39",
"url": "http://www.tour-eiffel.fr",
"visited" : false
},
{
"title": "Parc Guell",
"place": "Barcelona",
"latitude": 41.414483,
"longitude": 2.152579,
"information": "Parc Guell is a garden complex with architectural elements situated on the hill of El Carmel in the Gràcia district of Barcelona.",
"telephone": "+34 902 20 03 02",
"url": "http://www.parkguell.es",
"visited" : false
}
]
}
This is how I am reading and parsing it:
func readLocationData() {
let bundle = NSBundle.mainBundle()
let path = bundle.pathForResource("locations", ofType: "json")
let content : NSString = NSString.stringWithContentsOfFile(path) as NSString
let locationData:NSData = content.dataUsingEncoding(NSUTF8StringEncoding)
var error: NSError?
var jsonDict:Dictionary = NSJSONSerialization.JSONObjectWithData(locationData, options: nil, error: &error) as Dictionary<String, AnyObject>
if let locations : AnyObject = jsonDict["locations"] {
if let information: String = locations["information"] {
// I get error here: Could not find an overload for 'subscript' that accepts the supplied arguments
}
}
}
Now how do I get access to internal keys like title, place and location? Anything I try inside the loop gives a casting error like:
Could not find an overload for 'subscript' that accepts the supplied arguments
Let's look at what types the different parts of the JSON are: jsonDict is a dictionary that has an array as its only value, and jsonDict["locations"] is an array of other dictionaries. You should be using Dictionary<String, Any> for jsonDict and then you'll have better luck later:
var jsonDict:Dictionary = NSJSONSerialization.JSONObjectWithData(locationData, options: nil, error: &error) as Dictionary<String, Any>
let locations: NSArray? = jsonDict["locations"] as? NSArray
if locations {
for location: AnyObject in locations! {
if let information = location as? NSDictionary {
println(information)
}
}
}
You have a dictionary with contains a single entry of "locations" which is an array of dictionaries.
Try this:
// I'm using NSDictionary since it is easier to work with when you don't know the object // // types but you can convert it later with explicit data types
if let results = jsonDict["locations"] as NSDictionary[] {
for item in results {
var title = item["title"] as String
}
}
Note: I haven't ran this code myself but you I think you should get the idea.