Swift Json decoding nested array / dictionary to flat model - ios

I am trying to decode the following json object to my User model in Swift.
My issue is decoding the values _id and token out of the tokens array, where the first token in the array contains the values I want to decode into User.tokenId and User.token.
I am trying to extract/map the values directly into my User model struct without having another nested struct in my User model ( such as struct Token { var id: String , var token: String } )
let json = """
{
"currentLocation": {
"latitude": 0,
"longitude": 0
},
"profileImageUrl": "",
"bio": "",
"_id": "601453e4aae564fc19075b68",
"username": "johnsmith",
"name": "john",
"email": "johnsmith#gmail.com",
"keywords": ["word", "weds"],
"tokens": [
{
"_id": "213453e4aae564fcqu775b69",
"token": "eyJhbGciOiJIUzqoNiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MDE0NTNlNGFhZTU2NGZjMTkwNzViNjgiLCJpYXQiOjE2MTE5NDQ5MzIsImV4cCI6MTYxMjM3NjkzMn0.PbTsA3B0MAfcVvEF1UAMhUXFiqIL1FcxVFGgMTZ5HCk"
}
],
"createdAt": "2021-01-29T18:28:52.845Z",
"updatedAt": "2021-01-29T18:28:52.883Z"
}
""".data(using: .utf8)!
struct User: Codable {
var latitude: Double
var longitude: Double
var profileImageUrl: String
var bio: String
var userId: String
var username: String
var name: String
var email: String
var keywords: [String]
var tokenId: String
var token: String
var createdAt: Date
var updatedAt: Date
private enum UserKeys: String, CodingKey {
case currentLocation
case profileImageUrl
case bio
case userId = "_id"
case username
case name
case email
case keywords
case tokens
case createdAt
case updatedAt
}
private enum CurrentLocationKeys: String, CodingKey {
case latitude
case longitude
}
private enum TokenKeys: String, CodingKey {
case tokenId = "_id"
case token
}
init(from decoder: Decoder) throws {
let userContainer = try decoder.container(keyedBy: UserKeys.self)
let currentLocationContainer = try userContainer.nestedContainer(keyedBy: CurrentLocationKeys.self, forKey: .currentLocation)
self.latitude = try currentLocationContainer.decode(Double.self, forKey: .latitude)
self.longitude = try currentLocationContainer.decode(Double.self, forKey: .longitude)
self.profileImageUrl = try userContainer.decode(String.self, forKey: .profileImageUrl)
self.bio = try userContainer.decode(String.self, forKey: .bio)
self.userId = try userContainer.decode(String.self, forKey: .userId)
self.username = try userContainer.decode(String.self, forKey: .username)
self.name = try userContainer.decode(String.self, forKey: .name)
self.email = try userContainer.decode(String.self, forKey: .email)
self.keywords = try userContainer.decode([String].self, forKey: .keywords)
let tokensContainer = try userContainer.nestedContainer(keyedBy: TokenKeys.self, forKey: .tokens)
self.tokenId = try tokensContainer.decode(String.self, forKey: .tokenId)
self.token = try tokensContainer.decode(String.self, forKey: .token)
self.createdAt = try userContainer.decode(Date.self, forKey: .createdAt)
self.updatedAt = try userContainer.decode(Date.self, forKey: .updatedAt)
}
}
let user = try! decoder.decode(User.self, from: json)

First of all I assume that your decoder has an appropriate date decoding strategy to be able to decode the ISO8601 strings to Date.
The enclosing container of the token dictionary is an array. You have to insert an intermediate nestedUnkeyedContainer
...
var arrayContainer = try userContainer.nestedUnkeyedContainer(forKey: .tokens)
let tokensContainer = try arrayContainer.nestedContainer(keyedBy: TokenKeys.self)
self.tokenId = try tokensContainer.decode(String.self, forKey: .tokenId)
self.token = try tokensContainer.decode(String.self, forKey: .token)
...
It's much less code to decode the JSON into multiple structs

Related

Decoding fails if keys not present

Decoding fails if keys not present. How to safely decode if missing keys also.
I have gone through that use Optional or Nil values but still not able to decoding the objects.
Below my Json
{
"mandatory":true,
"dynamic_obj":[
{
"dt":"2021-09-22 01:29:52",
"url":"https://res._22_01_29.pdf",
"desc":"PAN CARD",
"flag":1,
"count":"2",
"field":"pan_card",
"address":"300-435, Nattu Muthu St, Sheethammal Colony, Venus Colony, Chennai, Tamil Nadu 600018, India",
"visible":true,
"latitude":13.0389309,
"longitude":80.2473746
},
{
"url":"https://res.cloudin/no-image.jpg",
"desc":"driving License",
"count":"2",
"field":"driving_license",
"visible":true
}
]
}
Model class below
struct Dynamic_obj : Codable {
var dt : String?
var url : String?
let desc : String?
var flag : Int?
let count : String?
let field : String?
let visible : Bool?
var bankname : String = "NA"
var pdfPassword : String = "NA"
var latitude : String = "NA"
var longitude : String = "NA"
var address : String = "NA"
enum CodingKeys: String, CodingKey {
case dt = "dt"
case url = "url"
case desc = "desc"
case flag = "flag"
case count = "count"
case field = "field"
case visible = "visible"
case bankname = "bankname"
case pdfPassword = "pdfPassword"
case latitude = "latitude"
case longitude = "longitude"
case address = "address"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
dt = try values.decodeIfPresent(String.self, forKey: .dt)
url = try values.decodeIfPresent(String.self, forKey: .url)
desc = try values.decodeIfPresent(String.self, forKey: .desc)
flag = try values.decodeIfPresent(Int.self, forKey: .flag)
count = try values.decodeIfPresent(String.self, forKey: .count)
field = try values.decodeIfPresent(String.self, forKey: .field)
visible = try values.decodeIfPresent(Bool.self, forKey: .visible)
bankname = try values.decodeIfPresent(String.self, forKey: .bankname) ?? "NA"
pdfPassword = try values.decodeIfPresent(String.self, forKey: .pdfPassword) ?? "NA"
latitude = try values.decodeIfPresent(String.self, forKey: .latitude) ?? "NA"
longitude = try values.decodeIfPresent(String.self, forKey: .longitude) ?? "NA"
address = try values.decodeIfPresent(String.self, forKey: .address) ?? "NA"
}
}
let decoder = JSONDecoder()
do {
let responseModel = try decoder.decode(LoanDocxPending.self, from: data)
if let mandotory = responseModel.mandatory{
self.visibleDocuments["Identity Proof-~id_proof"] = idProof
self.visibleTitle.append("Identity Proof")
}
} catch {
print("error")
}
struct LoanDocxPending :Codable {
let mandatory : Bool?
var dynamic_obj : [Dynamic_obj]?
enum CodingKeys: String, CodingKey {
case mandatory = "mandatory"
case dynamic_obj = "dynamic_obj"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
mandatory = try values.decodeIfPresent(Bool.self, forKey: .mandatory)
dynamic_obj = try values.decodeIfPresent([Dynamic_obj].self, forKey: .dynamic_obj)
}
}
First of all most of your code is not needed, for example the init methods and almost all CodingKeys.
Second of all as mentioned in the comments rather than printing meaningless literal "error" print the error instance. It will tell you that latitude and longitude are Double, not String.
This model matches the JSON in the question
struct DynamicObj : Codable {
let dt : String?
let url, desc : String
let flag : Int?
let count, field : String
let visible : Bool
var bankname, pdfPassword, address : String?
var latitude, longitude : Double?
}
struct LoanDocxPending :Codable {
let mandatory : Bool
var dynamicObj : [DynamicObj]
enum CodingKeys: String, CodingKey {
case mandatory,dynamicObj = "dynamic_obj"
}
}
let decoder = JSONDecoder()
do {
let responseModel = try decoder.decode(LoanDocxPending.self, from: data)
print(responseModel.mandatory)
} catch {
print("error", error)
}

Swift Decode unique keys in JSON or ignore certain keys

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)

How to construct Codable from response data structure

How to do codable for Any & JSONNull from response data
Json Response Data
"userType": {
"id": "5f18J20a21n",
"name": "userName",
"name_prefix": null,
"group": []
}
UserType Model Class
struct UserType : Codable {
let id : String?
let name : String?
let namePrefix: JSONNull? // I replace JSONNull with String, Is it good way to do so?
let group : [JSONAny?]?
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case namePrefix = "name_prefix"
case group = "group"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id)
name = try values.decodeIfPresent(String.self, forKey: .name)
namePrefix = try values.decodeIfPresent(String.self, forKey: . namePrefix)
// namePrefix = try values.decodeIfPresent(JSONNull.self, forKey: . namePrefix)
group = try values.decodeIfPresent([JSONAny].self, forKey: .group)
// Type of expression is ambiguous without more context
}
}
I try above code which gives an error
Type of expression is ambiguous without more context

GitHub API not parsing

This is a part of the JSON i am getting from Github in response to a request
{
"total_count": 1657,
"incomplete_results": false,
"items": [
{
"id": 68911683,
"node_id": "MDEwOlJlcG9zaXRvcnk2ODkxMTY4Mw==",
"name": "tetros",
"full_name": "daniel-e/tetros",
"private": false,
"html_url": "https://github.com/daniel-e/tetros",
"description": "Tetris that fits into the boot sector.",
"size": 171,
"stargazers_count": 677,
"watchers_count": 677,
"language": "Assembly",
}
]
}
This is my Model
struct RepoGroup:Codable {
var items:[Repo]
}
struct Repo: Codable {
var fullName:String
var stars:Int
var watchers:Int
init(url:String,star:Int,watcher:Int) {
fullName = url
stars = star
watchers = watcher
}
enum MyStructKeys: String, CodingKey {
case fullName = "full_name"
case stars = "stargazers_count"
case watchers = "watchers_count"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MyStructKeys.self)
let fullName: String = try container.decode(String.self, forKey: .fullName)
let stars: Int = try container.decode(Int.self, forKey: .stars)
let watchers: Int = try container.decode(Int.self, forKey: .watchers)
self.init(url: fullName, star: stars, watcher: watchers)
}
}
So far so good. But as soon as i add description:String field in my model, the JSON decoder inexplicably fails to parse.
Here is my parser
let model = try JSONDecoder().decode(RepoGroup.self, from: dataResponse)
I am struggling to understand what is so special about the description field. Any kind of help would be greatly appreciated. Thanks.
Description appears to be an optional field in the GitHub API, and when a repo doesn't define a description, it is coming back as a null. This means you need to make your description field a String? and switch to using decodeIfPresent to account for the fact that it is optional.
Nothing seems off about that particular JSON to not work for description. Haven't tested this, but this is what your code looks like?
struct RepoGroup:Codable {
var items:[Repo]
}
struct Repo: Codable {
var fullName:String
var stars:Int
var watchers:Int
var description:String
init(url:String,star:Int,watcher:Int,description:String) {
fullName = url
stars = star
watchers = watcher
description = description
}
enum MyStructKeys: String, CodingKey {
case fullName = "full_name"
case stars = "stargazers_count"
case watchers = "watchers_count"
case description = "description"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MyStructKeys.self)
let fullName: String = try container.decode(String.self, forKey: .fullName)
let stars: Int = try container.decode(Int.self, forKey: .stars)
let watchers: Int = try container.decode(Int.self, forKey: .watchers)
let description: String = try container.decode(String.self, forKey: .description)
self.init(url: fullName, star: stars, watcher: watchers, description: description)
}
}

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.

Resources