1
votes

Swift Codable: comment "passer" un sous-objet JSON non analysé

Pour des raisons compliquées, je me retrouve à travailler à contre-courant de Codable : lors du décodage d'un objet json je souhaite conserver le sous-objet sous la clé extra "tout comme json ", stocké dans un dictionnaire [String: Any] , mais [String: Any] n'est (bien sûr) pas Decodable . Y a-t-il un moyen de faire cela?

La première réponse est bien sûr "ne pas". A quoi ma réponse est "Je dois": je dois faire deux Codable passes sur les données, dans lesquelles le premier décode une liste d'objets hétérogènes (qui une clé name ) tandis que la deuxième passe utilise un dictionnaire indexé sur ces valeurs de name et est correctement de type sécurisé. La première passe ne peut pas être de type sécurisé car elle fonctionne sur une liste hétérogène, mais elle doit conserver toutes les données avec lesquelles la seconde passe fonctionnera. Heureusement, toutes les données hétérogènes sont cachées sous cette clé extra , mais je ne vois toujours pas comment faire.

(Il y aura très probablement une question de suivi sur le _en_codage des mêmes choses, donc si vous avez des informations, n'hésitez pas à le mentionner.)


5 commentaires

Pouvez-vous avoir deux ensembles de votre hiérarchie de structure Codable , un sans le extra et un avec extra mais d'autres éléments supprimés, puis en utiliser un pour le premier décodage et l'autre pour le deuxième décodage? En pensant à voix haute ici, je n'ai jamais rien essayé de tel.


avez-vous essayé le type [String: AnyHashable]?


@JoakimDanielson Je pense que la deuxième passe doit fonctionner avec la sortie de la première, donc je ne pense pas que je puisse sauter complètement extra . Merci pour la suggestion cependant, elle roule dans ma tête et pourrait encore susciter un nouvel aperçu.


@Rishabh pas Codable (pour la raison habituelle liée à l'effacement de type ou à l'hétérogénéité: l'encodage est correct mais lors du décodage il n'y a aucun moyen de dire quoi mettre dans le AnyHashable )


Vérifiez ceci: forums.developer.apple.com/thread/80288 cela ressemble à un problème similaire et il y a une réponse là aussi.


3 Réponses :


0
votes

Le plus proche que j'ai obtenu était le code Playgrounds suivant:

{"bar":"{\"someInt\":1}"}
{"bar":"{"someInt":1}"}
Foo(bar: ["someInt": 1])

Les impressions ci-dessus:

struct Foo: Codable {
    let bar: [String: Any]

    enum CodingKeys: String, CodingKey {
        case bar
    }

    init(bar: [String: Any]) {
        self.bar = bar
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let barString = try values.decode(String.self, forKey: .bar)
        let barData = Data(barString.utf8)
        let json: [String: Any] = try JSONSerialization.jsonObject(with: barData, options: .allowFragments) as! [String: Any]
        bar = json
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        let barData = try JSONSerialization.data(withJSONObject: bar, options: .sortedKeys)
        let barString = String(decoding: barData, as: UTF8.self)
        try container.encode(barString, forKey: .bar)
    }
}

let foo = Foo(bar: ["someInt": 1])
let fooData = try! JSONEncoder().encode(foo)
print(String(decoding: fooData, as: UTF8.self))
print(String(decoding: fooData, as: UTF8.self).replacingOccurrences(of: "\\\"", with: "\""))

let decodedFoo = try! JSONDecoder().decode(Foo.self, from: fooData)
print(decodedFoo)

Ce n'est pas encore parfait, car il échappe le " dans la chaîne JSON résultante, donc cela cassera probablement quelque chose là-dedans. Le passage de remplacement de chaîne supplémentaire n'est probablement pas quelque chose qui est agréable.


1 commentaires

Cette approche peut probablement fonctionner (avec quelques ajustements pour l'échappement) pour aller-retour Swift-> JSON-> Swift, mais pas pour décoder le JSON produit par le serveur: la forme codée enveloppe nécessairement le sous-objet de la barre dans une chaîne, ce que le serveur ne fera pas. Si l'aller-retour est la seule exigence, simplement encoder / décoder la barre en tant que Data via JSONSerialization est probablement plus simple.



3
votes

Vous pouvez créer un dictionnaire personnalisé décodable qui décode et stocke les valeurs dans un Dictionary , puis utilisez ce type pour la clé dans laquelle vous souhaitez stocker le dictionnaire brut.

Voici le décodable:

name
["bool": true, "string": "rt", "int": 1, "float": 1.12, "dict": ["test": "String"]]

Vous pouvez maintenant utiliser cette structure pour décoder le dictionnaire comme ceci:

let data = """
{
    "name": "name",
    "data": {
        "string": "rt",
        "bool": true,
        "float": 1.12,
        "int": 1,
        "dict": {
            "test": "String"
        }
    }
}
"""

let s = try JSONDecoder().decode(Test.self, from: data.data(using: .utf8)!)
print(s.name)
print(s.data)

Testons:

struct Test: Decodable {
    let name: String
    let data: [String: Any]

    enum Keys: String, CodingKey {
        case name
        case data
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Keys.self)
        name = try container.decode(String.self, forKey: .name)
        data = try container.decode(DictionaryDecodable.self, forKey: .data).dictionary // Here use DictionaryDecodable
    }
}

Voici le résultat:

struct DictionaryDecodable: Decodable {

    let dictionary : [String: Any]

    private struct Key : CodingKey {

        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }

    init(from decoder: Decoder) throws {
        let con = try decoder.container(keyedBy: Key.self)
        var dict = [String: Any]()
        for key in con.allKeys {
            if let value = try? con.decode(String.self, forKey:key) {
                dict[key.stringValue] = value
            } else if let value = try? con.decode(Int.self, forKey:key) {
                dict[key.stringValue] = value
            } else if let value = try? con.decode(Double.self, forKey:key) {
                dict[key.stringValue] = value
            } else if let value = try? con.decode(Bool.self, forKey:key) {
                dict[key.stringValue] = value
            } else if let data = try? con.decode(DictionaryDecodable.self, forKey:key)  {
                dict[key.stringValue] = data.dictionary
            }

        }
        self.dictionary = dict
    }
}


2 commentaires

Vous pouvez vous débarrasser du init écrit manuellement pour Test si vous déclarez la propriété comme DictionaryDecodable . L'ajout de [Bool ], [String] , etc. à la liste des tentatives de décodage aiderait.


Oui, nous pouvons le faire, alors à chaque point d'accès, vous devez utiliser .dictionary



0
votes

Ce Forums Swift le post semble apporter une solution. Cela commence ainsi:

public enum JSON : Codable {
    case null
    case number(NSNumber)
    case string(String)
    case array([JSON])
    case dictionary([String : JSON])
    // ...

L'idée est de faire un type enum JSON explicite, qui est Codable , encodant directement la structure json ( chaîne de cas (String); tableau de cas ([JSON]); etc.). Le sous-objet non analysé est fortement typé en JSON et peut être encodé / décodé via Codable , et (en prime) peut également être analysé avec "just Swift" ( allumez le boîtier et faites des choses) si nécessaire.


0 commentaires