Decode custom JSON with Decodable + Realm Swift - ios

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.

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

How to discriminate while decoding using Codable Protocol in Swift?

I am using Swift's Codable Protocol. I have shared the code.
I want the variable boss in the class Employee to acquire the type based on the personType String in the Person class. I want to use personType as discriminator. Response coming from the server will be different every time based on the personType value.
In Employee class, I have declared the boss variable with Person type. I want it to decode for type Employee if the personType string in the Person class is "Employee" and decode for type Boss if the personType string is "Boss". If it is null I simply want it to decode for type Person.
Any help would be really appreciated.
public class Person: Codable {
public let address: String
public let age: Int
public let name: String
public let uid: String
public let personType: String?
private enum CodingKeys: String, CodingKey {
case address
case age
case name
case uid
case personType
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
address = try container.decode(String.self, forKey: .address)
age = try container.decode(Int.self, forKey: .age)
name = try container.decode(String.self, forKey: .name)
uid = try container.decode(String.self, forKey: .uid)
personType = try container.decodeIfPresent(String.self, forKey: .personType)
}
}
public class Employee: Person {
public let department: String
public let dependents: [Person]?
public let salary: Int
public let workingDays: [Days]
public var boss: Person?
private enum CodingKeys: String, CodingKey {
case department
case dependents
case salary
case workingDays
case boss
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
department = try container.decode(String.self, forKey: .department)
dependents = try container.decode([Person].self, forKey: .dependents)
salary = try container.decode(Int.self, forKey: .salary)
workingDays = try container.decode([Days].self, forKey: .workingDays)
boss = try container.decode(Person.self, forKey: .boss)
try super.init(from: decoder)
}
}
public class Boss: Employee {
let promotedAt: Double
let assistant: Employee?
enum CodingKeys: String, CodingKey {
case promotedAt
case assistant
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
promotedAt = try container.decode(Double.self, forKey: .promotedAt)
assistant = try container.decodeIfPresent(Employee.self, forKey: .assistant)
try super.init(from: decoder)
}
}
For example in the following response, in the boss section, personType is set to 'Boss'. So it should be decoded to the Boss type. If it were 'Employee' it should automatically decode to Employee or if it null is should decode to 'Person'.
{ name: 'Shahid Khaliq',
age: 5147483645,
address: 'H # 531, S # 20',
uid: '123321',
salary: 20000,
department: 'Software Development',
workingDays: [ 'Monday', 'Tuesday', 'Friday' ],
boss:
{ personType: 'Boss',
assistant: null,
name: 'Zeeshan Ejaz',
age: 5147483645,
address: 'H # 531, S # 20',
uid: '123321',
birthday: '1994-02-13',
birthtime: '1994-02-13T14:01:54.000Z',
salary: 20000,
department: 'Software Development',
joiningDay: 'Saturday',
workingDays: [ 'Monday', 'Tuesday', 'Friday' ],
dependents: null,
hiredAt: 'Sun, 06 Nov 1994 08:49:37 GMT',
boss: null,
promotedAt: 1484719381 },
dependents: null,
hiredAt: 'Sun, 06 Nov 1994 08:49:37 GMT',
personType: null }
Need to change model as per current response you posted
public class Employee: Person {
public let department: String
public let dependents: [Person]?
public let salary: Int
public let workingDays:[String] //[Days]
public var boss: Person?
private enum CodingKeys: String, CodingKey {
case department
case dependents
case salary
case workingDays
case boss
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
department = try container.decode(String.self, forKey: .department)
//dependents = try container.decode([Person].self, forKey: .dependents)
dependents = try container.decodeIfPresent([Person].self, forKey: .dependents)
salary = try container.decode(Int.self, forKey: .salary)
// workingDays = try container.decode([Days].self, forKey: .workingDays)
workingDays = try container.decode([String].self, forKey: .workingDays)
// boss = try container.decode(Person.self, forKey: .boss)
boss = try container.decodeIfPresent(Person.self, forKey: .boss)
try super.init(from: decoder)
}
}
here added decodeIfPresent in few properties as it null as per current response
while decoding you can use different thing, i have used SwiftJSON to make code more readable ,
// i have saved response in Response.JSON file so can change response as per need while testing below code.
let file = Bundle.main.path(forResource: "Response", ofType: "JSON")
let dataURL = URL(fileURLWithPath: file!)
let data = try! Data(contentsOf: dataURL)
let jsonData = try! JSON(data: data)
//here JSON is struct which is part of SwiftyJSON
print("jsondata \(jsonData)")
do {
let bossDict = jsonData["boss"]
let dataBoss : Data = try! bossDict.rawData()
let bossType = bossDict["personType"].string
if let type = bossType {
if type == "Boss"{
let bossObj = try! JSONDecoder().decode(Boss.self, from: dataBoss)
print("boss name \(String(describing: bossObj.name))")
bossObj.listPropertiesWithValues()
}else{
// type == "Employee"
let emplyeeObj = try! JSONDecoder().decode(Employee.self, from: dataBoss)
print("Employee name \(String(describing: emplyeeObj.name))")
emplyeeObj.listPropertiesWithValues()
}
}else{
//type = nil
}
}catch{
print("exception \(error)")
}
You can download working demo of the same at link
DemoCodable
I would add a switch statement at the end of the init(from decoder:) in the Employee class
switch personType {
case "Boss":
boss = try container.decode(Boss.self, forKey: .boss)
case "Employee":
boss = try container.decode(Employee.self, forKey: .boss)
default:
boss = nil
}

How to get the nondecoded attributes from a Decoder container in Swift 4?

I'm using the Decodable protocol in order to parse JSON received from an external source. After decoding the attributes that I do know about there still may be some attributes in the JSON that are unknown and have not yet been decoded. For example, if the external source added a new attribute to the JSON at some future point in time I would like to hold onto these unknown attributes by storing them in a [String: Any] dictionary (or an alternative) so the values do not get ignored.
The issue is that after decoding the attributes that I do know about there isn't any accessors on the container to retrieve the attributes that have not yet been decoded. I'm aware of the decoder.unkeyedContainer() which I could use to iterate over each value however this would not work in my case because in order for that to work you need to know what value type you're iterating over but the value types in the JSON are not always identical.
Here is an example in playground for what I'm trying to achieve:
// Playground
import Foundation
let jsonData = """
{
"name": "Foo",
"age": 21
}
""".data(using: .utf8)!
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// I would like to store the `age` attribute in this dictionary
// but it would not be known at the time this code was written.
self.unknownAttributes = [:]
}
}
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
// The `person.unknownAttributes` dictionary should
// contain the "age" attribute with a value of 21.
I would like for the unknownAttributes dictionary to store the age attribute and value in this case and any other possible value types if they get added to the JSON from the external source in the future.
The reason I am wanting to do something like this is so that I can persist the unknown attributes present in the JSON so that in a future update of the code I will be able to handle them appropriately once the attribute keys are known.
I've done plenty of searching on StackOverflow and Google but haven't yet encountered this unique case. Thanks in advance!
You guys keep coming up with novel ways to stress the Swift 4 coding APIs... ;)
A general solution, supporting all value types, might not be possible. But, for primitive types, you can try this:
Create a simple CodingKey type with string-based keys:
struct UnknownCodingKey: CodingKey {
init?(stringValue: String) { self.stringValue = stringValue }
let stringValue: String
init?(intValue: Int) { return nil }
var intValue: Int? { return nil }
}
Then write a general decoding function using the standard KeyedDecodingContainer keyed by the UnknownCodingKey above:
func decodeUnknownKeys(from decoder: Decoder, with knownKeys: Set<String>) throws -> [String: Any] {
let container = try decoder.container(keyedBy: UnknownCodingKey.self)
var unknownKeyValues = [String: Any]()
for key in container.allKeys {
guard !knownKeys.contains(key.stringValue) else { continue }
func decodeUnknownValue<T: Decodable>(_ type: T.Type) -> Bool {
guard let value = try? container.decode(type, forKey: key) else {
return false
}
unknownKeyValues[key.stringValue] = value
return true
}
if decodeUnknownValue(String.self) { continue }
if decodeUnknownValue(Int.self) { continue }
if decodeUnknownValue(Double.self) { continue }
// ...
}
return unknownKeyValues
}
Finally, use the decodeUnknownKeys function to fill your unknownAttributes dictionary:
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let knownKeys = Set(container.allKeys.map { $0.stringValue })
self.unknownAttributes = try decodeUnknownKeys(from: decoder, with: knownKeys)
}
}
A simple test:
let jsonData = """
{
"name": "Foo",
"age": 21,
"token": "ABC",
"rate": 1.234
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
print(person.name)
print(person.unknownAttributes)
prints:
Foo
["age": 21, "token": "ABC", "rate": 1.234]

Swift Decodable Optional Key

(This is a follow-up from this question: Using Decodable protocol with multiples keys.)
I have the following Swift code:
let additionalInfo = try values.nestedContainer(keyedBy: UserInfoKeys.self, forKey: .age)
age = try additionalInfo.decodeIfPresent(Int.self, forKey: .realage)
I know that if I use decodeIfPresent and don't have the property it will still handle it correctly if it's an optional variable.
For example the following JSON works to parse it using the code above.
{
"firstname": "Test",
"lastname": "User",
"age": {"realage": 29}
}
And the following JSON works as well.
{
"firstname": "Test",
"lastname": "User",
"age": {"notrealage": 30}
}
But the following doesn't work.
{
"firstname": "Test",
"lastname": "User"
}
How can I make all 3 examples work? Is there something similar to decodeIfPresent for nestedContainer?
You can use the following KeyedDecodingContainer function:
func contains(_ key: KeyedDecodingContainer.Key) -> Bool
Returns a Bool value indicating whether the decoder contains a value associated with the given key. The value associated with the given key may be a null value as appropriate for the data format.
For instance, to check if the "age" key exists before requesting the corresponding nested container:
struct Person: Decodable {
let firstName, lastName: String
let age: Int?
enum CodingKeys: String, CodingKey {
case firstName = "firstname"
case lastName = "lastname"
case age
}
enum AgeKeys: String, CodingKey {
case realAge = "realage"
case fakeAge = "fakeage"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.firstName = try values.decode(String.self, forKey: .firstName)
self.lastName = try values.decode(String.self, forKey: .lastName)
if values.contains(.age) {
let age = try values.nestedContainer(keyedBy: AgeKeys.self, forKey: .age)
self.age = try age.decodeIfPresent(Int.self, forKey: .realAge)
} else {
self.age = nil
}
}
}
I had this issue and I found this solution, just in case is helpful to somebody else:
let ageContainer = try? values.nestedContainer(keyedBy: AgeKeys.self, forKey: .age)
self.age = try ageContainer?.decodeIfPresent(Int.self, forKey: .realAge)
If you have an optional container, using try? values.nestedContainer(keyedBy:forKey) you don't need to check if the container exist using contains(.
Can you try pasting your sample JSON into quicktype to see what types it infers? Based on your question, I pasted your samples and got:
struct UserInfo: Codable {
let firstname: String
let age: Age?
let lastname: String
}
struct Age: Codable {
let realage: Int?
}
Making UserInfo.age and Age.realage optionals works, if that's what you're trying to accomplish.

Resources