6
votes

Utilisez Swift Codable pour décoder JSON avec des valeurs comme clés

J'ai un problème pour décoder une structure JSON que je ne peux pas modifier pour la rendre plus facile à décoder (elle vient de firebase) ..

Comment décoder le JSON suivant en objets? Le problème est de savoir comment convertir "7E7-M001". C'est le nom d'un conteneur qui a des tiroirs. Le nom des tiroirs est également utilisé comme clé.

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

Que dois-je corriger dans la classe Container & Drawer pour avoir la clé comme propriété title et un tableau d'objets dans ces classes?

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


0 commentaires

3 Réponses :


0
votes

Dans ce cas, nous ne pouvons pas créer de classes codables statiques pour ce JSON . Mieux vaut utiliser la sérialisation JSON et la récupérer.


1 commentaires

Vous pouvez créer des classes avec Codable mais vous devez écrire des initialiseurs personnalisés.



13
votes

Je vais d'abord faire quelques légères simplifications pour pouvoir me concentrer sur les points importants de cette question. Je vais rendre tout immuable, remplacer les classes par des structures et implémenter uniquement Decodable. Rendre cet encodable est un problème distinct.

L'outil central pour gérer les clés de valeur inconnue est un CodingKey qui peut gérer n'importe quelle chaîne:

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

Le deuxième outil important est la capacité pour connaître votre propre titre. Cela signifie demander au décodeur "où sommes-nous?" C'est le dernier élément du chemin de codage actuel.

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

Et puis nous avons besoin d'un moyen de décoder les éléments qui sont "intitulés" de cette façon:

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

Avec cela, nous pouvons inventer un protocole pour ces choses "intitulées" et les décoder:

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

Et c'est l'essentiel du travail. Nous pouvons utiliser ce protocole pour rendre le décodage assez facile pour les couches de niveau supérieur. Il suffit d'implémenter init(title:elements:).

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

Tool est un peu différent car c'est un nœud feuille et a d'autres choses à décoder.

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

Cela ne laisse que le niveau le plus élevé. Nous allons créer un type Containers juste pour terminer les choses.

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

Et pour l'utiliser, décoder les Containers code>:

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

Notez que puisque les objets JSON ne conservent pas l'ordre, les tableaux peuvent ne pas être dans le même ordre que le JSON, et peuvent ne pas être dans le même ordre entre les exécutions.

Gist


2 commentaires

Réponse incroyable


Voir ci-dessous J'ai élargi la réponse de Rob pour donner une réponse plus générale et lui donner plus de capacités.



2
votes

Je vais étendre la réponse de Rob pour donner une réponse plus générale et lui donner plus de capacités. Nous allons d'abord prendre un exemple Json et identifier tous les scénarios qui peuvent y être contenus.

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

À la fin, vous pourrez lire toutes les valeurs comme suit,

XXX

Donc, jusqu'à la clé d'adresse, vous pouvez facilement décoder. Mais après cela, vous aurez besoin d'une structure Object spécifique pour contenir toutes les données mappées par des paires clé-valeur dynamiques. Voici donc ma structure d'objet Swift suggérée. Supposons que le Json ci-dessus est pour 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)
    }
}

Concentrez-vous sur la façon dont le Json s'est transformé en structure Object. Voici le mécanisme extrait et amélioré de la réponse de Rob.

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)")
}


0 commentaires