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)!)
Related
I'm using REST APIs to retrieve data from my Firestore DB. I'm forced to use REST API instead of the Firebase SDK since App Clip don't allow to use the latter.
The JSON file is the following: JSON File
And, as text:
{
"name": "projects/myProject/databases/(default)/documents/Brand/rxnBLnp736gqjFBNLxxx",
"fields": {
"descrizione": {
"stringValue": "My project Brand Demo"
},
"descrizione_en": {
"stringValue": "My project Brand Demo"
},
"listaRefsLinea": {
"arrayValue": {
"values": [
{
"referenceValue": "projects/myProject/databases/(default)/documents/Linea/aeeDNuY9xEvRvyM5cxxx"
}
]
}
},
"data_consumption": {
"stringValue": "7xpISf0XxRnfrnUkNxxx"
},
"url_logo": {
"stringValue": "gs://myproject.appspot.com/FCMImages/app-demo-catalogue.png"
},
"web_url": {
"stringValue": "www.mybrand.it"
},
"nome_brand": {
"stringValue": "My project Demo"
}
},
"createTime": "2021-05-19T10:34:51.828685Z",
"updateTime": "2022-05-24T14:03:16.121296Z"
}
And I'm decoding it as follows:
import Foundation
struct BrandResponse : Codable {
let brands : [Brand_Struct]
private enum CodingKeys : String, CodingKey {
case brands = "documents"
}
}
struct StringValue : Codable {
let value : String
private enum CodingKeys : String, CodingKey {
case value = "stringValue"
}
}
struct Brand_Struct : Codable {
let url_logo : String
let web_url : String
let nome_brand : String
let descrizione : String
let listaRefsLinea : [String]
let descrizione_en : String
let data_consumption : String
private enum BrandKeys : String, CodingKey {
case fields
case listaRefsLinea
}
private enum FieldKeys : String, CodingKey {
case url_logo
case web_url
case nome_brand
case descrizione
case listaRefsLinea
case descrizione_en
case data_consumption
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: BrandKeys.self)
let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields)
//listaRefsLinea = try containerListaRefsLinea_2.decode(ArrayValue.self, forKey: .values).referenceValue
nome_brand = try fieldContainer.decode(StringValue.self, forKey: .nome_brand).value
web_url = try fieldContainer.decode(StringValue.self, forKey: .web_url).value
url_logo = try fieldContainer.decode(StringValue.self, forKey: .url_logo).value
descrizione = try fieldContainer.decode(StringValue.self, forKey: .descrizione).value
descrizione_en = try fieldContainer.decode(StringValue.self, forKey: .descrizione_en).value
data_consumption = try fieldContainer.decode(StringValue.self, forKey: .data_consumption).value
listaRefsLinea = [""] // <-- How to read this??
}
}
My issue is that I'm not being able to read the array inside the field "listaRefsLinea". Any idea on how to achieve that? Also I'm afraid that part of the troubles come from the fact that that's a Document Reference variable and as such does not conform to the Codable protocol.
Well. listaRefsLinea is a custom object just like your StringValue
So add these structs:
// MARK: - ListaRefsLinea
struct ListaRefsLinea: Codable {
let arrayValue: ArrayValue
}
// MARK: - ArrayValue
struct ArrayValue: Codable {
let values: [Value]
}
// MARK: - Value
struct Value: Codable {
let referenceValue: String
}
and in your custom init decode it to this struct, go down the tree until you get the array and map that to a [String]:
listaRefsLinea = try fieldContainer.decode(ListaRefsLinea.self, forKey: .listaRefsLinea)
.arrayValue.values.map{ $0.referenceValue }
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"
}
}
}
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")
Lets have a json
{
"channelId": 100,
"channel_name": "STV 1",
"stream": {
"URL": "www.rtvs.sk",
"DRM": "secureMedia",
"drmKeys": ["1", "2", "3"],
"userInfo": {
"user": "Michal23",
"userIsTester": true
}
}
}
and a struct:
struct Channel : Codable {
var channelId : Int
var channelName : String
var channelUrl : URL
private enum CodingKeys : String, CodingKey {
case channelId
case channelName = "channel_name"
case channelUrl = "URL" <===??? json path somehow?
}
}
I would like to fetch URL from nested stream, but without creating nested struct for it. Is it possible? How?
Looking at the documentation, you can do it but it is more of a manual process than usual. You need to decode the nested container and then extract the information using the coding key.
//: Playground - noun: a place where people can play
import UIKit
let jsonData = """
{
"channelId": 100,
"channel_name": "STV 1",
"stream": {
"URL": "www.rtvs.sk"
}
}
""".data(using: String.Encoding.utf8)!
struct Channel {
var channelId : Int
var channelName : String
var channelUrl: URL
private enum CodingKeys : String, CodingKey {
case channelId
case channelName = "channel_name"
case stream
}
private enum AdditionalInfoKeys: String, CodingKey {
case channelUrl = "URL"
}
}
extension Channel: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
channelId = try values.decode(Int.self, forKey: .channelId)
channelName = try values.decode(String.self, forKey: .channelName)
let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .stream)
channelUrl = try additionalInfo.decode(URL.self, forKey: .channelUrl)
}
}
let decoder = JSONDecoder()
let channel = try? decoder.decode(Channel.self, from: jsonData)
print(channel)
OUTPUT: Channel(channelId: 100, channelName: "STV 1", channelUrl: www.rtvs.sk))
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.