web-dev-qa-db-fra.com

Swift 4 décodable avec des clés inconnues jusqu'à l'heure du décodage

Comment le protocole Swift 4 Decodable) gère-t-il un dictionnaire contenant une clé dont le nom n'est pas connu avant l'exécution? Par exemple:

  [
    {
      "categoryName": "Trending",
      "Trending": [
        {
          "category": "Trending",
          "trailerPrice": "",
          "isFavourit": null,
          "isWatchlist": null
        }
      ]
    },
    {
      "categoryName": "Comedy",
      "Comedy": [
        {
          "category": "Comedy",
          "trailerPrice": "",
          "isFavourit": null,
          "isWatchlist": null
        }
      ]
    }
  ]

Nous avons ici un éventail de dictionnaires; le premier a les clés categoryName et Trending, tandis que le second a les clés categoryName et Comedy. La valeur de la clé categoryName me dit le nom de la deuxième clé. Comment puis-je exprimer cela en utilisant Decodable?

48
matt

La clé réside dans la définition de la propriété CodingKeys. Bien qu'il s'agisse le plus souvent d'un enum, il peut s'agir de tout ce qui est conforme au protocole CodingKey. Et pour créer des clés dynamiques, vous pouvez appeler une fonction statique:

struct Category: Decodable {
    struct Detail: Decodable {
        var category: String
        var trailerPrice: String
        var isFavorite: Bool?
        var isWatchlist: Bool?
    }

    var name: String
    var detail: Detail

    private struct CodingKeys: CodingKey {
        var intValue: Int?
        var stringValue: String

        init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
        init?(stringValue: String) { self.stringValue = stringValue }

        static let name = CodingKeys.make(key: "categoryName")
        static func make(key: String) -> CodingKeys {
            return CodingKeys(stringValue: key)!
        }
    }

    init(from coder: Decoder) throws {
        let container = try coder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.detail = try container.decode([Detail].self, forKey: .make(key: name)).first!
    }
}

Usage:

let jsonData = """
  [
    {
      "categoryName": "Trending",
      "Trending": [
        {
          "category": "Trending",
          "trailerPrice": "",
          "isFavourite": null,
          "isWatchlist": null
        }
      ]
    },
    {
      "categoryName": "Comedy",
      "Comedy": [
        {
          "category": "Comedy",
          "trailerPrice": "",
          "isFavourite": null,
          "isWatchlist": null
        }
      ]
    }
  ]
""".data(using: .utf8)!

let categories = try! JSONDecoder().decode([Category].self, from: jsonData)

(J'ai changé isFavourit dans le JSON en isFavourite car je pensais que c'était une faute de frappe. Il est assez facile d'adapter le code si ce n'est pas le cas)

47
Code Different

Vous pouvez écrire une structure personnalisée qui fonctionne comme un objet CodingKeys et l'initialiser avec une chaîne telle qu'elle extrait la clé que vous avez spécifiée:

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

Ainsi, une fois que vous savez quelle est la clé souhaitée, vous pouvez dire (dans la init(from:) _ override:

let key = // whatever the key name turns out to be
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)

Donc ce que j’ai fini par faire est de faire deux conteneurs à partir du décodeur - un en utilisant l’énumération CodingKeys standard pour extraire la valeur du "categoryName" key, et un autre utilisant la structure CK pour extraire la valeur de la clé dont nous venons d'apprendre le nom:

init(from decoder: Decoder) throws {
    let con = try! decoder.container(keyedBy: CodingKeys.self)
    self.categoryName = try! con.decode(String.self, forKey:.categoryName)
    let key = self.categoryName
    let con2 = try! decoder.container(keyedBy: CK.self)
    self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
}

Voici donc toute ma structure Decodable:

struct ResponseData : Codable {
    let categoryName : String
    let unknown : [Inner]
    struct Inner : Codable {
        let category : String
        let trailerPrice : String
        let isFavourit : String?
        let isWatchList : String?
    }
    private enum CodingKeys : String, CodingKey {
        case categoryName
    }
    private struct CK : 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: CodingKeys.self)
        self.categoryName = try! con.decode(String.self, forKey:.categoryName)
        let key = self.categoryName
        let con2 = try! decoder.container(keyedBy: CK.self)
        self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
    }
}

Et voici le banc d'essai:

    let json = """
      [
        {
          "categoryName": "Trending",
          "Trending": [
            {
              "category": "Trending",
              "trailerPrice": "",
              "isFavourit": null,
              "isWatchlist": null
            }
          ]
        },
        {
          "categoryName": "Comedy",
          "Comedy": [
            {
              "category": "Comedy",
              "trailerPrice": "",
              "isFavourit": null,
              "isWatchlist": null
            }
          ]
        }
      ]
    """
    let myjson = try! JSONDecoder().decode(
        [ResponseData].self, 
        from: json.data(using: .utf8)!)
    print(myjson)

Et voici le résultat de l'instruction print, prouvant que nous avons correctement rempli nos structures:

[JustPlaying.ResponseData(
    categoryName: "Trending", 
    unknown: [JustPlaying.ResponseData.Inner(
        category: "Trending", 
        trailerPrice: "", 
        isFavourit: nil, 
        isWatchList: nil)]), 
 JustPlaying.ResponseData(
    categoryName: "Comedy", 
    unknown: [JustPlaying.ResponseData.Inner(
        category: "Comedy", 
        trailerPrice: "", 
        isFavourit: nil, 
        isWatchList: nil)])
]

Bien sûr, dans la vraie vie, nous aurions un peu de gestion des erreurs, sans aucun doute!


EDIT Plus tard, j'ai réalisé (en partie grâce à la réponse de CodeDifferent) que je n'avais pas besoin de deux conteneurs; Je peux éliminer l'énumération CodingKeys et ma structure CK peut faire tout le travail! C'est un fabricant de clés à usage général:

init(from decoder: Decoder) throws {
    let con = try! decoder.container(keyedBy: CK.self)
    self.categoryName = try! con.decode(String.self, forKey:CK(stringValue:"categoryName")!)
    let key = self.categoryName
    self.unknown = try! con.decode([Inner].self, forKey: CK(stringValue:key)!)
}
5
matt

aussi, a posé cette question. Voici ce qui a finalement été trouvé pour ce JSON:

let json = """
{
    "BTC_BCN":{
        "last":"0.00000057",
        "percentChange":"0.03636363",
        "baseVolume":"47.08463318"
    },
    "BTC_BELA":{
        "last":"0.00001281",
        "percentChange":"0.07376362",
        "baseVolume":"5.46595029"
    }
}
""".data(using: .utf8)!

Nous faisons une telle structure:

struct Pair {
    let name: String
    let details: Details

    struct Details: Codable {
        let last, percentChange, baseVolume: String
    }
}

Quand décoder:

if let pairsDictionary = try? JSONDecoder().decode([String: Pair.Details].self, from: json) {

    var pairs: [Pair] = []
    for (name, details) in pairsDictionary {
        let pair = Pair(name: name, details: details)
        pairs.append(pair)
    }

    print(pairs)
}

Il est également possible d'appeler non pair.details.baseVolume, mais pair.baseVolume:

struct Pair {
    ......
    var baseVolume: String { return details.baseVolume }
    ......

Ou écrivez init personnalisé:

struct Pair {
    .....
    let baseVolume: String
    init(name: String, details: Details) {
         self.baseVolume = details.baseVolume
    ......
1
vbb