Swift Decode unique keys in JSON or ignore certain keys - ios

I'm practicing my Swift skills, this time I'm trying to make a Covid-19 tracker, and for this I found this API, the thing is, that the format retrieved by /cases is something like this (changing keys to make it more readable)
{
"Country1": {
"All": {
"property1": 0,
"property2": "foo"
}
}, {
"All": {
"property1": "0",
"property2": "bar",
},
"State1": {
"property1": 0,
"property3": "foobar"
}
}
}
And I made the following structs to decode it:
Country
struct Country: Codable {
let All: [String: All]
private enum CodingKeys: String, CodingKey {
case All
}
}
All
struct All: Codable {
let confirmed: Int?
let recovered: Int?
let deaths: Int?
let country: String?
let population: Int?
let squareKmArea: Int?
let lifeExpectancy: String?
var elevationInMeters: String?
let continent: String?
let location: String?
let iso: String?
let capitalCity: String?
let lat: String?
let long: String?
let updated: String?
private enum CodingKeys: String, CodingKey {
case confirmed
case recovered
case deaths
case country
case population
case squareKmArea = "sq_km_area"
case lifeExpectancy = "life_expectancy"
case elevationInMeters = "elevation_in_meters"
case continent
case location
case iso
case capitalCity = "capital_city"
case lat
case long
case updated
}
init(confirmed: Int?, recovered: Int?, deaths: Int?, country: String?, population: Int?,
squareKmArea: Int?, lifeExpectancy: String?, elevationInMeters: String?,
continent: String?, location: String?, iso: String?, capitalCity: String?,
lat: String?, long: String?, updated: String?) {
self.confirmed = confirmed
self.recovered = recovered
self.deaths = deaths
self.country = country
self.population = population
self.squareKmArea = squareKmArea
self.lifeExpectancy = lifeExpectancy
self.elevationInMeters = elevationInMeters
self.continent = continent
self.location = location
self.iso = iso
self.capitalCity = capitalCity
self.lat = lat
self.long = long
self.updated = updated
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.confirmed = try container.decodeIfPresent(Int.self, forKey: .confirmed)
self.recovered = try container.decodeIfPresent(Int.self, forKey: .recovered)
self.deaths = try container.decodeIfPresent(Int.self, forKey: .deaths)
self.country = try container.decodeIfPresent(String.self, forKey: .country)
self.population = try container.decodeIfPresent(Int.self, forKey: .population)
self.squareKmArea = try container.decodeIfPresent(Int.self, forKey: .squareKmArea)
self.lifeExpectancy = try container.decodeIfPresent(String.self, forKey: .lifeExpectancy)
self.elevationInMeters = try container.decodeIfPresent(String.self, forKey: .elevationInMeters)
self.continent = try container.decodeIfPresent(String.self, forKey: .continent)
self.location = try container.decodeIfPresent(String.self, forKey: .location)
self.iso = try container.decodeIfPresent(String.self, forKey: .iso)
self.capitalCity = try container.decodeIfPresent(String.self, forKey: .capitalCity)
self.lat = try container.decodeIfPresent(String.self, forKey: .lat)
self.long = try container.decodeIfPresent(String.self, forKey: .long)
self.updated = try container.decodeIfPresent(String.self, forKey: .updated)
do {
self.elevationInMeters = try String(container.decodeIfPresent(Int.self, forKey: .elevationInMeters) ?? 0)
} catch DecodingError.typeMismatch {
print("Not a number")
self.elevationInMeters = try container.decodeIfPresent(String.self, forKey: .elevationInMeters) ?? ""
}
}
}
The elevation in meters can come in either String or Int value (4+ digits long > String (appends a comma), otherwise Int) (Idea taken from this answer
And I try to decode it this way
if let decodedData = self.jsonUtilities.decode(json: safeData, as: [String : Country].self) {
print(decodedData)
}
My above decode method looks like this:
func decode<T: Decodable>(json: Data, as clazz: T.Type) -> T? {
do {
let decoder = JSONDecoder()
let data = try decoder.decode(T.self, from: json)
return data
} catch {
print(error)
print("An error occurred while parsing JSON")
}
return nil
}
I found this answer where they suggest adding all the Coding Keys there, but these are a ton.
I haven't added all the keys, as it's an 8k+ lines JSON, so I was wondering if there's an easier way to decode it, as I can't think of a better way to decode this JSON with unique keys.
Alternatively if I could ignore all the keys that aren't "All" might also work, as I'm just trying to get the totals per country and their locations to place them in a map.
So far I get this error:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Diamond Princess", intValue: nil), CodingKeys(stringValue: "All", intValue: nil), _JSONKey(stringValue: "recovered", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found a number instead.", underlyingError: nil))
An error occurred while parsing JSON
And afaik it's because it's not finding the key "Diamond Princess" which is a state (or so I believe) in Canada (according to my JSON), because I haven't added it for the reasons above.

When you have dynamic keys, you could decode it as [String: ...] dictionary.
The structure of the JSON is as follows:
{
"Canada": {
"All": { ... },
"Ontario": { ... },
...
},
"Mexico": { ... },
...
}
All the country stats have the key "All" and if that's all you need then you could create the following structs:
struct Country: Decodable {
var all: CountryAll
enum CodingKeys: String, CodingKey {
case all = "All"
}
}
struct CountryAll: Decodable {
var confirmed: Int
var recovered: Int
//.. etc
}
and decode as:
let countries = try JSONDecoder().decode([String: Country].self, from: json)

Related

Filtering empty values from an API in swift

I'm trying to filter out empty and null values from an api in a json format in swift(UIKit).
The full data returns look like below but sometimes can contain null or empty values in the characteristic key. There is always going to be the same amount of keys.
//Cat
{
"breedname": "Persian",
"picture": "https://catimage.random.png",
"characteristic1": "Shy",
"characteristic2": "Hungry all the time"
"characteristic3": "Likes apples"
"characteristic4": "Grey color"
"characteristic5": "likes chin scratches"
}
{
"breedname": "Bengal",
"picture": "https://catimage.random.png",
"characteristic1": "Active",
"characteristic2": "Adventurous"
"characteristic3": ""
"characteristic4": ""
"characteristic5": ""
}
{
"breedname": "ragdoll",
"picture": "https://catimage.random.png",
"characteristic1": "Fiestey",
"characteristic2": "sharp claws"
"characteristic3": null
"characteristic4": null
"characteristic5": null
}
In order to filter null and empty values before showing in the UI, I have a Decodable class like below and a custom init class with the decodeifPresent method which changes null values to nill. However for empty values I just created a method which converts empty string values to nill. I'm not sure if there are better ways to handle empty and null data and filtering them out? I refer to all the Decodable keys in the UI so I cannot simply delete the keys themselves.
struct Cat: Decodable {
let breedName: String
let picture: String
let characteristic1 : String?
let characteristic2 : String?
let characteristic3 : String?
let characteristic4 : String?
let characteristic5 : String?
enum CodingKeys: String, CodingKey {
case breedName
case picture
case characteristic1
case characteristic2
case characteristic3
case characteristic4
case characteristic5
}
func checkEmpty(s: String?) -> String? {
if s == "" {
return nil
}
return s
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.breedName= try container.decode(String.self, forKey: .breedName)
self.picture = try container.decode(String.self, forKey: .picture)
self.characteristic1 = try container.decodeIfPresent(String.self, forKey: .characteristic1)
self.characteristic2 = try container.decodeIfPresent(String.self, forKey: .characteristic2)
self.characteristic3 = try container.decodeIfPresent(String.self, forKey: .characteristic3)
self.characteristic4 = try container.decodeIfPresent(String.self, forKey: .characteristic4)
self.characteristic5 = try container.decodeIfPresent(String.self, forKey: .characteristic5)
self.characteristic1 = checkEmpty(s: self.characteristic1)
self.characteristic2 = checkEmpty(s: self.characteristic2)
self.characteristic3 = checkEmpty(s: self.characteristic3)
self.characteristic4 = checkEmpty(s: self.characteristic4)
self.characteristic5 = checkEmpty(s: self.characteristic5)
One solution is to check for empty in a function defined in an extension to String
extension String {
func emptyAsNil() -> String? {
self.isEmpty ? nil : self
}
}
Then you could do all in one step in the init
self.characteristic1 = try container.decodeIfPresent(String.self, forKey: .characteristic1)?.emptyAsNil()
But perhaps a better solution is to gather all those properties in a collection like an array or a dictionary. Here I have chosen an array
struct Cat: Decodable {
let breedName: String
let picture: String
var characteristics: [String]
}
and then in the init we add only non-nil, non-empty values to the array
if let value = try container.decodeIfPresent(String.self, forKey: .characteristic1), !value.isEmpty {
characteristics.append(value)
}
or another way is to loop over the keys
let keys: [CodingKeys] = [.characteristic1,
.characteristic2,
.characteristic3,
.characteristic4,
.characteristic5]
for key in keys {
if let value = try container.decodeIfPresent(String.self, forKey: key), !value.isEmpty {
characteristics.append(value)
}
}

How to decode DynamicKeys & CodingKeys in the same container?

Consider the following JSON: I'm trying to decode the "teams" object.
let jsonString = """
{
"Superheroes":{
"Marvel":"107",
"DC":"106"
},
"teams":{
"106":{
"name":"Marvel",
"Superheroes":{
"890":{
"name":"Batman"
}
}
},
"107":{
"name":"DC",
"Superheroes":{
"891":{
"name":"Wonder Woman"
}
}
}
}
}
"""
I have tried something like this:
struct SuperheroResponse: Decodable {
let teams: [Team]
private enum CodingKeys: String, CodingKey {
case teams = "teams"
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.teams)
print(teamContainer.allKeys.count)
let tempArray: [Team] = []
for key in teamContainer.allKeys {
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
tempArray.append(decodedObject)
}
teams = tempArray
}
}
struct Team: Decodable {
let name: String
}
I thought that first I would get the teams container, map over the keys and go on from there. Problem is teamContainer.allKeys.count is always zero.
Also the following line, results in following error: Cannot convert value of type 'SuperheroResponse.DynamicCodingKeys' to expected argument type 'SuperheroResponse.CodingKeys'
let decodedObject = try teamContainer.decode(Team.self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
Finally I decode it as follows:
let jsonData = Data(jsonString.utf8)
let decodedResult = try! JSONDecoder().decode(SuperheroResponse.self, from: jsonData)
dump(decodedResult)
Any help would be appreciated. Ideally I would like something like SuperheroResponse -> [Team],
Team -> name, [Superhero], Superhero -> name
You just have a couple of minor mistakes. You're almost there.
The team container is keyed by DynamicCodingKeys:
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, // <=
forKey: .teams)
And the Teams can be decoded as using the key you're given:
let decodedObject = try teamContainer.decode(Team.self, forKey: key)
Also, tempArray needs to be var:
var tempArray: [Team] = []
Or replace that loop with a map:
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
All together:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let teamContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .teams)
teams = try teamContainer.allKeys.map {
try teamContainer.decode(Team.self, forKey: $0)
}
}

JSON Decoder Type Mismatch Error in Swift

When I try to decode JSON I get the error:
typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1972", intValue: 1972), CodingKeys(stringValue: "song_name", intValue: nil)], debugDescription: "Expected to decode String but found a number instead.", underlyingError: nil))
JSON:
[
{
"song_id": 1373123,
"song_name": "신라의 달밤",
"artist": "현인",
"album": "현인",
"Like_count": 100,
"Lyric": "아~ 신라의 밤이여\n불국사의 종소리 들리어 온다\n지나가는 나그네야 걸음을 멈추어라\n고요한 달빛 어린 금오산 기슭에서\n노래를 불러보자 신라의 밤 노래를\n\n아~ 신라의 밤이여\n아름다운 궁녀들 그리워라\n대궐 뒤에 숲 속에서 사랑을 맺었던가\n님들의 치맛소리 귓속에 들으면서\n노래를 불러보자 신라의 밤 노래를",
"cover_url": "https://image.bugsm.co.kr/album/images/200/1134/113497.jpg?version=20170925032718.0"
},
...
Swift code
let decoder = JSONDecoder()
do {
self.songs = try decoder.decode([Song].self, from: asset.data)
} catch {
print(error)
}
Model
struct Song: Codable {
// var id: Int
var title: String
var artist: String
// var album: String
// var like: Int
// var lylic: String
// var coverURL: String
enum CodingKeys: String, CodingKey {
// case id = "song_id"
case title = "song_name"
case artist = "artist"
// case album = "album"
// case like = "Like_count"
// case lylic = "Lyric"
// case coverURL = "cover_url"
}
}
The error is crystal clear: In the 1973rd item of the array (index 1972) the value for key song_name is a number. You have to add a custom initializer to fix the issue for example
struct Song: Decodable {
let id : Int
let title : String
// ... other struct members
enum CodingKeys: String, CodingKey {
case id = "song_id"
case title = "song_name"
// ... other CodingKeys
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
do {
self.title = try container.decode(String.self, forKey: .title)
} catch {
let numberName = try container.decode(Int.self, forKey: .title)
self.title = String(numberName)
}
// other lines to initialize the other struct members
}
}
If the error still persists the number could be also Double or Bool.

Decode custom JSON with Decodable + Realm Swift

From the server I have a big JSON returned that looks something like this:
{
"id": "123",
"status": "ok",
"person": {
"administration": {
"name": "John"
}
},
"company": {
"name": "Test"
}
}
I have a struct:
struct Info: Decodable, Object {
let id: String
let status: String
let personName: String
let companyName: String
}
It conforms to Decodable protocol and also is a Object (Realm entity).
My question is: Am I able somehow to decode the name of the person in personName? Something like person.administration.name.
I want the end Realm Object, to be a flat one and mostly all of the fields are strings.
Should I create separate structs for Person/Company without being Realm Objects and in decode method to set the corresponding value to "personName"?
let personName: String = try container.decode((Person.Administration.name).self, forKey: .personName)
You can simply use containers to decode nested data with Decodable, i.e.
struct Info: Decodable {
let id: String
let status: String
let personName: String
let companyName: String
enum CodingKeys: String, CodingKey {
case id, status
case person, administration
case company
case name
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
status = try values.decode(String.self, forKey: .status)
//Decoding personName
let person = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .person)
let administration = try person.nestedContainer(keyedBy: CodingKeys.self, forKey: .administration)
personName = try administration.decode(String.self, forKey: .name)
//Decoding companyName
let company = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .company)
companyName = try company.decode(String.self, forKey: .name)
}
}
Example:
I've decoded the JSON you provided above, i.e.
if let data = json.data(using: .utf8) {
let info = try? JSONDecoder().decode(Info.self, from: data)
print(info)
}
The output it gives is:
(id: "123", status: "ok", personName: "John", companyName: "Test")
You can separate out the CodingKeys for all the different levels as per your wish. I kept them at the same level for simplicity.
Suggestion: Try using the optional types with Codable. This is because the API response can be unexpected. And if you don't get any expected key-value pair, you might end up getting a nil while creating the object.
It is best practice to separate transport types you're parsing your JSON into and types to represent object in the storage.
But if you want to use this combined types you should do something like this:
struct Info: Decodable {
let id: String
let status: String
let personName: String
let companyName: String
// JSON root keys
private enum RootKeys: String, CodingKey {
case id, status, person, company
}
// Keys for "person" nested "object"
private enum PersonKeys: String, CodingKey {
case administration
}
// Keys for "administration" and "company"
private enum NamedKeys: String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.status = try container.decode(String.self, forKey: .status)
let personContainer = try container.nestedContainer(keyedBy: PersonKeys.self, forKey: .person)
let administrationContainer = try personContainer.nestedContainer(keyedBy: NamedKeys.self, forKey: .administration)
self.personName = try administrationContainer.decode(String.self, forKey: .name)
let companyContainer = try container.nestedContainer(keyedBy: NamedKeys.self, forKey: .company)
self.companyName = try companyContainer.decode(String.self, forKey: .name)
}
}
I separated keys into three different CodingKey types for some type safety, and to prevent accidental mixup.

Parsing dissimilar type json response

I have a json response like so…
{
"message_code": 1,
"orderCount": 52,
"productCount": 5,
"outstandingPayment": [],
"pendingOrder": [
{
"order_id": 1,
"grand_total": 67.85
"customer_name": “xcvv”
"mobile_number": 2147483647
},
],
"bestWishes": [
{
"customer_id": 1,
"birth_date": "2018-02-02",
"type": "birth_date",
"customer_name": “xcvv”,
"mobile_number": 2147483647
},
{
"customer_id": 1,
"anniversary_date": "2018-02-02",
"type": "anniversary_date",
"customer_name": “sdfs”,
"mobile_number": 2147483647
}
]
}
To parse pendingOrder I have made a struct like so:
struct PendingOrder: Codable {
let order_id: Int
let grand_total: Double
let customer_name: String
let mobileNo: Int
init(order_id : Int, grand_total: Double, customer_name: String, mobileNo: Int) {
self.order_id = order_id
self.grand_total = grand_total
self.customer_name = customer_name
self.mobileNo = mobileNo
}
}
But how can I make a struct for bestWishes since each dictionary has dissimilar data i.e.the 1st dictionary has a field birth_date & the 2nd dictionary has a field anniversary_date...?
EDIT: While making Alamofire request, this is how I'm parsing each data and assigning them to struct..
if let bestWishes = result["bestWishes"] as? [[String:Any]] {
for anItem in bestWishes {
guard let customerId = anItem["customer_id"] as? Int,
let birthdate = anItem["birth_date"] as? String,
let customerName = anItem["customer_name"] as? String,
let mobNo = anItem["mobile_number"] as? Int,
let anniversaryDate = anItem["anniversary_date"] as? String,
let type = anItem["type"] as? String
else {continue}
let bestWishes = BestWishes(customer_id: customerId, birthDate: birthdate, type: type, customer_name: customerName, mobileNo: mobNo, anniversaryDate: anniversaryDate)
self.bestWishesArr.append(bestWishes)
First of all when using Codable basically you don't need an initializer.
Without a custom initializer (related to Codable) the most suitable solution is to declare both dates as optional
struct BestWish: Codable {
private enum CodingKeys : String, CodingKey {
case customerID = "customer_id"
case birthDate = "birth_date"
case anniversaryDate = "anniversary_date"
case type
case customerName = "customer_name"
case mobileNumber = "mobile_number"
}
let customerID: Int
let birthDate: String?
let anniversaryDate: String?
let type: String
let customerName : String
let mobileNumber: Int
}
With a custom initializer declare one property date and decode anniversary or birth depending on type
struct BestWish: Decodable {
private enum CodingKeys : String, CodingKey {
case customerID = "customer_id"
case birthDate = "birth_date"
case anniversaryDate = "anniversary_date"
case type
case customerName = "customer_name"
case mobileNumber = "mobile_number"
}
let customerID: Int
let date: Date
let type: String
let customerName : String
let mobileNumber: Int
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
customerID = try container.decode(Int.self, forKey: .customerID)
type = try container.decode(String.self, forKey: .type)
if type == "anniversary_date" {
date = try container.decode(Date.self, forKey: .anniversaryDate)
} else {
date = try container.decode(Date.self, forKey: .birthDate)
}
customerName = try container.decode(String.self, forKey: .customerName)
mobileNo = try container.decode(Int.self, forKey: .mobileNumber)
}
}
You can distinguish the dates by the given type property.
To decode also the root object you need an umbrella struct
struct Root : Decodable {
// let pendingOrder : [PendingOrder]
let bestWishes : [BestWish]
}
Rather than the deserialized dictionary you have to get the JSON string in Data format from the Alamofire request to pass it to JSONDecoder
do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
let result = try decoder.decode(Root.self, from: data)
print(result)
} catch { print(error) }
Edit: I added code to decode both dates as Date
Use optional type
struct BestWishes: Codable {
let birth_date: Date?
let anniversary_date: Date?
init(birth_date : Date?, anniversary_date: Date?) {
self.birth_date = birth_date
self.anniversary_date = anniversary_date
}
}

Resources