Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use swift Codable to decode JSON with values as keys

I have a problem decoding a JSON structure which I cannot change to make it easier to decode (it's coming from firebase)..

How do I decode the following JSON into objects? The problem is how to convert "7E7-M001". It's the name of a container which has drawers. The drawers name is also used as a key.

{
  "7E7-M001" : {
    "Drawer1" : {
      "101" : {
        "Partnumber" : "F101"
      },
      "102" : {
        "Partnumber" : "F121"
      }
    }
  },
  "7E7-M002": {
    "Drawer1": {
      "201": {
        "Partnumber": "F201"
      },
      "202": {
        "Partnumber": "F221"
      }
    }
  }
}

What do I have to fix in the Container & Drawer class to have the key as a title property and an array of objects in these classes ?

class Container: Codable {
    var title: String
    var drawers: [Drawer]
}

class Drawer: Codable {
    var title: String
    var tools: [Tool]
}

class Tool: Codable {
    var title: String
    var partNumber: String

    enum CodingKeys: String, CodingKey {
        case partNumber = "Partnumber"
    }
}
like image 936
Tycho Pandelaar Avatar asked Jan 10 '19 13:01

Tycho Pandelaar


People also ask

How do I parse JSON in Codable Swift?

To do this, you have to convert your Swift objects to JSON. This is called encoding. To decode or encode data, your objects must conform to the Codable protocol. Then you can use JSONDecoder() or JSONEncoder() objects to decode/encode the data.

What does Codable mean in Swift?

Codable is the combined protocol of Swift's Decodable and Encodable protocols. Together they provide standard methods of decoding data for custom types and encoding data to be saved or transferred.


2 Answers

First I'm going to make some slight simplifications so I can focus on the important points of this question. I'm going to make everything immutable, replace the classes with structs, and only implement Decodable. Making this Encodable is a separate issue.

The central tool for handling unknown value keys is a CodingKey that can handle any string:

struct TitleKey: CodingKey {
    let stringValue: String
    init?(stringValue: String) { self.stringValue = stringValue }
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }
}

The second important tool is the ability to know your own title. That means asking the decoder "where are we?" That's the last element in the current coding path.

extension Decoder {
    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
    }
}

And then we need a way to decode elements that are "titled" this way:

extension Decoder {
    func decodeTitledElements<Element: Decodable>(_ type: Element.Type) throws -> [Element] {
        let titles = try container(keyedBy: TitleKey.self)
        return try titles.allKeys.map { title in
            return try titles.decode(Element.self, forKey: title)
        }
    }
}

With that, we can invent a protocol for these "titled" things and decode them:

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.decodeTitledElements(Element.self))
    }
}

And that's most of the work. We can use this protocol to make decoding pretty easy for the upper-level layers. Just implement init(title:elements:).

struct Drawer: TitleDecodable {
    let title: String
    let tools: [Tool]
    init(title: String, elements: [Tool]) {
        self.title = title
        self.tools = elements
    }
}

struct Container: TitleDecodable {
    let title: String
    let drawers: [Drawer]

    init(title: String, elements: [Drawer]) {
        self.title = title
        self.drawers = elements
    }
}

Tool is a little different since it's a leaf node and has other things to decode.

struct Tool: Decodable {
    let title: String
    let partNumber: String

    enum CodingKeys: String, CodingKey {
        case partNumber = "Partnumber"
    }

    init(from decoder: Decoder) throws {
        self.title = try decoder.currentTitle()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.partNumber = try container.decode(String.self, forKey: .partNumber)
    }
}

That just leaves the very top level. We'll create a Containers type just to wrap things up.

struct Containers: Decodable {
    let containers: [Container]
    init(from decoder: Decoder) throws {
        self.containers = try decoder.decodeTitledElements(Container.self)
    }
}

And to use it, decode the top level Containers:

let containers = try JSONDecoder().decode(Containers.self, from: json)
print(containers.containers)

Note that since JSON objects are not order-preserving, the arrays may not be in the same order as the JSON, and may not be in the same order between runs.

Gist

like image 79
Rob Napier Avatar answered Oct 24 '22 05:10

Rob Napier


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.
        "[email protected]": { // [email protected] -> 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 "[email protected]": { 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))
    }
}
like image 3
Nadeesha Lakmal Avatar answered Oct 24 '22 04:10

Nadeesha Lakmal