I'm going to extend Rob's answer to give a more general answer and to give it more capabilities. First we'll take an example Json and identify all the scenarios that can be contained within.
let json = Data("""
{
"id": "123456", // id -> primitive data type that can be decoded normally
"name": "Example Name", // name -> primitive data type that can be decoded
"address": { // address -> key => static, object => has static key-value pairs
"city": "Negombo",
"country": "Sri Lanka"
},
"email": { // email -> key => static, object => has only one key-value pair which has a dynamic key. When you're sure, user can have only one email.
"example@gmail.com": { // example@gmail.com -> key => dynamic key, object => in this example the object is
// normal decodable object. But you can have objects that has dynamic key-value pairs.
"verified": true
}
},
"phone_numbers": { // phone_numbers -> key => static, object => has multiple key-value pairs which has a dynamic keys. Assume user can have multiple phone numbers.
"+94772222222": { // +94772222222 -> key => dynamic key, object => in this example the object is
// normal decodable object. But you can have objects that has dynamic key-value pairs.
"isActive": true
},
"+94772222223": { // +94772222223 -> key => another dynamic key, object => another object mapped to dynamic key +94772222223
"isActive": false
}
}
}
""".utf8)
At the end you will be able to read all the values as follows,
let decoder = JSONDecoder()
do {
let userObject = try decoder.decode(UserModel.self, from: json)
print("User ID : \(String(describing: userObject.id))")
print("User Name : \(String(describing: userObject.name))")
print("User Address city : \(String(describing: userObject.address?.city))")
print("User Address country: \(String(describing: userObject.address?.country))")
print("User Email. : \(String(describing: userObject.email?.emailContent?.emailAddress))")
print("User Email Verified : \(String(describing: userObject.email?.emailContent?.verified))")
print("User Phone Number 1 : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers.first?.number))")
print("User Phone Number 2 : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers[1].number))")
print("User Phone Number 1 is Active : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers.first?.isActive))")
print("User Phone Number 2 is Active : \(String(describing: userObject.phoneNumberDetails?.phoneNumbers[1].isActive))")
} catch {
print("Error deserializing JSON: \(error)")
}
So up to address key, you can easily Decode. But after that you're gonna need a specific Object structure to hold all the data mapped by dynamic key-value pairs.
So here is my suggested Swift Object structure. Assume the above Json is for UserModel.
import Foundation
struct UserModel: Decodable {
let id: String
let name: String
let address: Address?
let email: Email?
let phoneNumberDetails: PhoneNumberDetails?
enum CodingKeys: String, CodingKey {
case id
case name
case address
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.address = try? container.decode(Address.self, forKey: .address)
// ["email": Value] -> static key => Email Swift Object
// ["email": Value] -> only object => email.emailContent. Here Value has only one object.
self.email = try decoder.decodeStaticTitledElement(with: TitleKey(stringValue: "email")!, Email.self)
// ["phone_numbers": Value] -> static key => PhoneNumberDetails Swift Object
// ["phone_numbers": Value] -> multiple objects => phoneNumberDetails.phoneNumbers. Here Value has multiples objects.
self.phoneNumberDetails = try decoder.decodeStaticTitledElement(with: TitleKey(stringValue: "phone_numbers")!, PhoneNumberDetails.self)
}
}
struct Address: Decodable {
let city: String
let country: String
enum CodingKeys: String, CodingKey {
case city
case country
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.city = try container.decode(String.self, forKey: .city)
self.country = try container.decode(String.self, forKey: .country)
}
}
/*
* Extends SingleTitleDecodable.
* Object that was mapped to static key "email".
* SingleTitleDecodable uses when you know the Parent object has only one dynamic key-value pair
* In this case Parent object is "email" object in the json, and "example@gmail.com": { body } is the only dynamic key-value pair
* key-value pair is mapped into EmailContent
*/
struct Email: SingleTitleDecodable {
let emailContent: EmailContent?
init(title: String, element: EmailContent?) {
self.emailContent = element
}
}
struct EmailContent: Decodable {
let emailAddress: String
let verified: Bool
enum CodingKeys: String, CodingKey {
case verified
}
init(from decoder: Decoder) throws {
self.emailAddress = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.verified = try container.decode(Bool.self, forKey: .verified)
}
}
/*
* Extends TitleDecodable.
* Object that was mapped to static key "phone_numbers".
* TitleDecodable uses when you know the Parent object has multiple dynamic key-value pair
* In this case Parent object is "phone_numbers" object in the json, and "+94772222222": { body }, "+94772222222": { body } are the multiple dynamic key-value pairs
* Multiple dynamic key-value pair are mapped into PhoneNumber array
*/
struct PhoneNumberDetails: TitleDecodable {
let phoneNumbers: [PhoneNumber]
init(title: String, elements: [PhoneNumber]) {
self.phoneNumbers = elements
}
}
struct PhoneNumber: Decodable {
let number: String
let isActive: Bool
enum CodingKeys: String, CodingKey {
case isActive
}
init(from decoder: Decoder) throws {
self.number = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.isActive = try container.decode(Bool.self, forKey: .isActive)
}
}
Focus on how the Json has transformed into the Object structure. Here is the mechanism extracted and improved from Rob's answer.
import Foundation
/*
* This is to handle unknown keys.
* Convert Keys with any String value to CodingKeys
*/
struct TitleKey: CodingKey {
let stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
extension Decoder {
/*
* Decode map into object array that is type of Element
* [Key: Element] -> [Element]
* This will be used when the keys are dynamic and have multiple keys
* Within type Element we can embed relevant Key using => 'try decoder.currentTitle()'
* So you can access Key using => 'element.key'
*/
func decodeMultipleDynamicTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element] {
var decodables: [Element] = []
let titles = try container(keyedBy: TitleKey.self)
for title in titles.allKeys {
if let element = try? titles.decode(Element.self, forKey: title) {
decodables.append(element)
}
}
return decodables
}
/*
* Decode map into optional object that is type of Element
* [Key: Element] -> Element?
* This will be used when the keys are dynamic and when you're sure there'll be only one key-value pair
* Within type Element we can embed relevant Key using => 'try decoder.currentTitle()'
* So you can access Key using => 'element.key'
*/
func decodeSingleDynamicTitledElement<Element: Decodable>(_ type: Element.Type) throws -> Element? {
let titles = try container(keyedBy: TitleKey.self)
for title in titles.allKeys {
if let element = try? titles.decode(Element.self, forKey: title) {
return element
}
}
return nil
}
/*
* Decode map key-value pair into optional object that is type of Element
* Key: Element -> Element?
* This will be used when the root key is known, But the value is constructed with Maps where the keys can be Unknown
*/
func decodeStaticTitledElement<Element: Decodable>(with key: TitleKey, _ type: Element.Type) throws -> Element? {
let titles = try container(keyedBy: TitleKey.self)
if let element = try? titles.decode(Element.self, forKey: key) {
return element
}
return nil
}
/*
* This will be used to know where the Element is in the Object tree
* Returns the Key of the Element which was mapped to
*/
func currentTitle() throws -> String {
guard let titleKey = codingPath.last as? TitleKey else {
throw DecodingError.dataCorrupted(.init(codingPath: codingPath, debugDescription: "Not in titled container"))
}
return titleKey.stringValue
}
}
/*
* Class that implements this Protocol, contains an array of Element Objects,
* that will be mapped from a 'Key1: [Key2: Element]' type of map.
* This will be used when the Key2 is dynamic and have multiple Key2 values
* Key1 -> Key1: TitleDecodable
* [Key2: Element] -> Key1_instance.elements
* Key2 -> Key1_instance.elements[index].key2
*/
protocol TitleDecodable: Decodable {
associatedtype Element: Decodable
init(title: String, elements: [Element])
}
extension TitleDecodable {
init(from decoder: Decoder) throws {
self.init(title: try decoder.currentTitle(), elements: try decoder.decodeMultipleDynamicTitledElements(Element.self))
}
}
/*
* Class that implements this Protocol, contains a variable which is type of Element,
* that will be mapped from a 'Key1: [Key2: Element]' type of map.
* This will be used when the Keys2 is dynamic and have only one Key2-value pair
* Key1 -> Key1: SingleTitleDecodable
* [Key2: Element] -> Key1_instance.element
* Key2 -> Key1_instance.element.key2
*/
protocol SingleTitleDecodable: Decodable {
associatedtype Element: Decodable
init(title: String, element: Element?)
}
extension SingleTitleDecodable {
init(from decoder: Decoder) throws {
self.init(title: try decoder.currentTitle(), element: try decoder.decodeSingleDynamicTitledElement(Element.self))
}
}