Swift - JSON Serialisation of an nested object - ios

I have a object that is created from JSON (serialised). This object has an id property that represents another object stored in the system. How can I get the nested object during the serialisation?
import UIKit
class Person: Codable {
let firstName: String
let lastName: String
let addressId: String
let address: Address // How to create it during serialisation
private enum CodingKeys: String, CodingKey {
case firstName
case lastName
case addressId = "addressId"
}
init(firstName: String, lastName: String, addressId:String) {
self.firstName = firstName
self.lastName = lastName
self.addressId = addressId
}
required 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)
self.addressId = try? values.decode(URL.self, forKey: .addressId)
}
}
struct PersonsList : Codable {
let persons: [Person]
}
class Address {
static func getAddress(addressId: String) -> Address
{
//some code
return address
}
}

Do it with lazy property
EDIT
Option1
lazy var address:Address = { [unowned self] in
return Address.getAddress(addressId: self.addressId)
}()
Option 2
var adreess1:Address {
return Address.getAddress(addressId: self.addressId)
}

JSON
Assuming this is your json
let json = """
[
{
"firstName": "James",
"lastName": "Kirk",
"address": { "id": "efg" }
}
]
"""
Model
You can simplify how you define your model
struct Person: Codable {
let firstName: String
let lastName: String
let address: Address
struct Address: Codable {
let id: String
}
}
As you can see there is no need to write your custom init(:from)
From JSON to Data
To test it not we are going to transform the json into a Data value.
let data = json.data(using: .utf8)!
Decoding
And finally we can decode the data
if let persons = try? JSONDecoder().decode([Person].self, from: data) {
print(persons.first?.address.id)
}
Output
Optional("efg")

Related

Convert Int to String while decoding JSON in Swift

I want to decode this JSON to a normal-looking Struct or Class but I'm facing a problem that I need to create a whole new Struct for property age, how can I avoid that and save age Directly to class Person?
Also, it would be good to convert Int to String
{
"name": "John",
"age": {
"age_years": 29
}
}
struct Person: Decodable {
var name: String
var age: Age
}
struct Age: Decodable {
var age_years: Int
}
I want to get rid of Age and save it like:
struct Person: Decodable {
var name: String
var age: String
}
You can try
struct Person: Decodable {
let name,age: String
private enum CodingKeys : String, CodingKey {
case name, age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
do {
let years = try container.decode([String:Int].self, forKey: .age)
age = "\(years["age_years"] ?? 0)"
}
catch {
let years = try container.decode([String:String].self, forKey: .age)
age = years["age_years"] ?? "0"
}
}
}

Parse JSON with different keys to same object using Codable in Swift

I receive the following 2 responses from different APIs
{
"id": "jdu72bdj",
"userInfo": {
"name": "Sudhanshu",
"age": 28,
"country": "India"
}
}
and
{
"profileId": "jdu72bdj",
"profileDetails": {
"name": "Sudhanshu",
"age": 28,
"country": "India"
}
}
This is in context with iOS development using Swift language.
Basically the object structure is same but keys are different. I'm parsing these using Codable, but I cannot think of a way to parse using same struct. All I can think of is making 2 different structs like this -
public struct Container1: Codable {
public let id: String
public let userInfo: UserProfile?
}
and
public struct Container2: Codable {
public let profileId: String
public let profileDetails: UserProfile?
}
They both use common UserProfile struct.
public struct UserProfile: Codable {
public let name: String?
public let age: Int?
public let country: String?
}
Is there a way to use one common container struct for both responses which parse response from 2 different keys. I do not want Container1 and Container2 since they both have same structure.
Any suggestions ?
One solution is to use a custom key decoding strategy using an implementation of CodingKey found in Apple's documentation. The idea is to map the keys of both of the json messages to the properties of the struct Container that will be used for both messages.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ keys in
let key = keys.last!.stringValue
switch key {
case "id", "profileId":
return AnyKey(stringValue: "id")!
case "userInfo", "profileDetails":
return AnyKey(stringValue: "details")!
default:
return keys.last!
}
})
where the custom implementation of CodingKey is
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
print(stringValue)
self.stringValue = stringValue
intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
and then decode both json messages the same way using the below struct
struct Container: Codable {
let id: String
let details: UserProfile
}
let result = try decoder.decode(Container.self, from: data)
You can use your own init from decoder
struct UniversalProfileContainer: Decodable {
struct UserProfile: Decodable {
var name: String
var age: Int
var country: String
}
enum CodingKeys: String, CodingKey {
case id = "id"
case profileId = "profileId"
case userInfo = "userInfo"
case profileDetails = "profileDetails"
}
let id: String
let profile: UserProfile
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try container.decode(String.self, forKey: .id)
} catch {
id = try container.decode(String.self, forKey: .profileId)
}
do {
profile = try container.decode(UserProfile.self, forKey: .userInfo)
} catch {
profile = try container.decode(UserProfile.self, forKey: .profileDetails)
}
}
}
let first = """
{
"id": "jdu72bdj",
"userInfo": {
"name": "Sudhanshu",
"age": 28,
"country": "India"
}
}
"""
let second = """
{
"profileId": "jdu72bdj",
"profileDetails": {
"name": "Sudhanshu",
"age": 28,
"country": "India"
}
}
"""
let response1 = try? JSONDecoder().decode(UniversalProfileContainer.self,
from: first.data(using: .utf8)!)
let response2 = try? JSONDecoder().decode(UniversalProfileContainer.self,
from: second.data(using: .utf8)!)

Splitting up an array and assigning values to each value in it

Im using Firestore and when I request data, it comes in an array
print("Document data: \(dataDescription)")
which prints out
Document data: ["lastname": Test, "firstname": Test]
I created a class
class User {
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
How can I assign the part of the array for "lastname" to the User class and assign in to lastName
A better way to do these parsings of Json to Model and vice versa is by using any parsing library e.g. ObjectMapper, SwiftyJson or swift's Codable protocol. You can do both way parsing with such an ease with them and even for complex data model you don't have to do a lot of work.
So better not to reinvent the wheel.
Here are some examples for your specific usecase.
let userData = ["lastname": "Last", "firstname": "First"]
Using ObjectMapper: (Install pod and add the import to get it work)
struct User: Mappable {
var firstname: String?
var lastname: String?
init?(map: Map) {}
mutating func mapping(map: Map) {
firstname <- map["firstname"]
lastname <- map["lastname"]
}
}
if let newuser = Mapper<User>().map(JSON: userData) {
print(newuser)
}
Using Codeable protocol:
struct User: Codable {
var firstname: String
var lastname: String
}
do {
let data = try JSONSerialization.data(withJSONObject: userData, options: .prettyPrinted)
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
} catch {
print(error.localizedDescription)
}
This
["lastname": Test, "firstname": Test]
is a dictionary not an array , you need
guard let res = snap.value as? [String:String] ,let fname = res["firstName"] ,
let lname = res["lastName"] else { return }
let user = User(firstName:fname,lastName:lname)
Tip:think if you really need a struct not class

Codable object mapping array element to string

I have a (annoying) situation where my back-end returns an object like this:
{
"user": {
"name": [
"John"
],
"familyName": [
"Johnson"
]
}
}
where each property is an array that holds a string as its first element. In my data model struct I could declare each property as an array but that really would be ugly. I would like to have my model as such:
struct User: Codable {
var user: String
var familyName: String
}
But this of course would fail the encoding/decoding as the types don't match. Until now I've used ObjectMapper library which provided a Map object and currentValue property, with that I could declare my properties as String type and in my model init method assig each value through this function:
extension Map {
public func firstFromArray<T>(key: String) -> T? {
if let array = self[key].currentValue as? [T] {
return array.first
}
return self[key].currentValue as? T
}
}
But now that I am converting to Codable approach, I don't know how to do such mapping. Any ideas?
You can override init(from decoder: Decoder):
let json = """
{
"user": {
"name": [
"John"
],
"familyName": [
"Johnson"
]
}
}
"""
struct User: Codable {
var name: String
var familyName: String
init(from decoder: Decoder) throws {
let container:KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self)
let nameArray = try container.decode([String].self, forKey: .name)
let familyNameArray = try container.decode([String].self, forKey: .familyName)
self.name = nameArray.first!
self.familyName = familyNameArray.first!
}
enum CodingKeys: String, CodingKey {
case name
case familyName
}
}
let data = json.data(using: .utf8)!
let decodedDictionary = try JSONDecoder().decode(Dictionary<String, User>.self, from: data)
print(decodedDictionary) // ["user": __lldb_expr_48.User(name: "John", familyName: "Johnson")]
let encodedData = try JSONEncoder().encode(decodedDictionary["user"]!)
let encodedStr = String(data: encodedData, encoding: .utf8)
print(encodedStr!) // {"name":"John","familyName":"Johnson"}
My tendency would be to adapt your model to the data coming in and create computed properties for use in the application, e.g.
struct User: Codable {
var user: [String]
var familyName: [String]
var userFirstName: String? {
return user.first
}
var userFamilyName: String? {
return familyName.first
}
}
This allows you to easily maintain parody with the data structure coming in without the maintenance cost of overriding the coding/decoding.
If it goes well with your design, you could also have a UI wrapper Type or ViewModel to more clearly differentiate the underlying Model from it's display.

How to initialize model class from JSON with custom type attribute

I know I can create failable initializer for my model class to initialize from JSON data by :
struct Person {
let firstName: String
let middleName: String?
init?(JSONData data:[String:AnyObject]) {
guard let firstName = data["firstName"] as? String else { return nil }
self.firstName = firstName
self.middleName = data["middleName"] as? String
}
}
But what if I have another attribute in Person which is another model class type? For example:
struct Person {
let firstName: String
let car: Car
init?(JSONData data:[String: AnyObject]) {
guard let firstName = data["firstName"] as! String,
let car = data["car"] as! Car // this line doesn't work I guess
else {return nil}
self.firstName = firstName
self.car = car
}
}
Car looks like this:
struct Car {
let year: Int
let brand: String
}
What is the proper way to make the failable initializer above work with custom type Car for JSON data parsing?
e.g. JSON:
{“firstName”: “John”,
“car”: {
“year”: 2009,
“brand”: “BMW”
}}
Firstly, you are not returning optionals in your guard statement. Change ! to ? in them.
Secondly, you should pass the data to the initializer and check if it is nil.
guard let firstName = data["firstName"] as? String,
let carData = data["car"] as? [String: AnyObject],
let car = Car(JSONData: carData)
else {return nil}
Change Car struct with this code-
struct Car {
let year: Int
let brand: String
init?(JSONData data:[String: AnyObject]) {
guard let brand = data["brand"] as! String,
let year = data["year"] as! Int
else {return nil}
self.brand = brand
self.year = year
}
}
And change Person struct to this-
struct Person {
let firstName: String
let car: Car
init?(JSONData data:[String: AnyObject]) {
guard let firstName = data["firstName"] as! String,
let car = Car(JSONData:data["car"])
else {return nil}
self.firstName = firstName
self.car = car
}
}
you can try like
struct Person {
let firstName: String
let car: Car
init?(JSONData data:[String: AnyObject]) {
guard let firstName = data["firstName"] as? String,
let carJSON = data["car"] as? [String: AnyObject?],
let car = Car(data: carJSON)
else {return nil}
self.firstName = firstName
self.car = car
}
}
and in Car
struct Car {
let year: Int
let brand: String
init?(data: [String: AnyObject?]) {
guard let brand = data["brand"] as? String,
let year = data["year"] as? Int
else {return nil}
self.brand = brand
self.year = year
}
}
As Swift 4+ introduces Codable For Encoding & decoding with external representation(JSON,Plist)
JsonData -
let jsonExample = """
{
"firstName": "John",
"car": {
"year": 2009,
"brand": "BMW"
}
}
""".data(using: .utf8)!
Struct with Codable protocol-
struct UserData: Codable{
var firstName: String
var car: CarData
}
struct CarData: Codable{
var year: Int
var brand: String
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
year = try values.decode(Int.self, forKey: .year)
brand = try values.decode(String.self, forKey: .brand)
}
}
Usage-
let jsonDecoder = JSONDecoder()
do {
let modelResult = try jsonDecoder.decode(UserData.self,from: jsonExample)
print("firstName is \(modelResult.firstName)")//prints John
print("car brand is \(modelResult.car.brand)")//Prints BMW
} catch {
print(error)
}
If of Json is with diffrent keys respective of your struct then use CodingKeys
Example-
let jsonExample = """
{
"firstNameOfBuyer": "John",
"car": {
"year-of-made": 2009,
"brand-name": "BMW"
}
}
""".data(using: .utf8)!
struct UserData: Codable {
var firstName: String
var car: CarData
private enum CodingKeys: String, CodingKey {
case firstName = "firstNameOfBuyer"
case car
}
}
struct CarData: Codable{
var year: Int
var brand: String
private enum CodingKeys: String, CodingKey {
case year = "year-of-made"
case brand = "brand-name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
year = try values.decode(Int.self, forKey: .year)
brand = try values.decode(String.self, forKey: .brand)
}
}

Resources