57 votes

Swift 4 Decodable avec des clés inconnues jusqu'au moment 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?

63voto

Zoff Dino Points 5010

La clé réside dans la définition de la propriété CodingKeys . Bien qu'il s'agisse le plus souvent de 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 code JSON en isFavourite depuis que je pensais que c'était une mauvaise orthographe. Il est assez facile d'adapter le code si ce n'est pas le cas)

8voto

matt Points 60113

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

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 ce que la touche souhaitée est, vous pouvez dire (en init(from:) remplacer:

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, c'est faire deux conteneurs du décodeur à l'aide de la norme CodingKeys enum pour extraire la valeur de l' "categoryName" - clés, et un autre à l'aide de la CK struct pour extraire la valeur de la clé dont le nom nous venons d'apprendre:

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

Ici, alors, c'est toute ma Décodable struct:

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 la sortie de l'instruction print, la preuve que nous avons peuplé nos structures correctement:

[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 une erreur de manipulation, pas de doute!


MODIFIER plus Tard, j'ai réalisé (en partie grâce à CodeDifferent de réponse) que je n'ai pas besoin de deux conteneurs; je peux éliminer la CodingKeys enum, et mon CK struct peut faire tout le travail! Il est d'un usage général, clé-maker:

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

4voto

vbb Points 21

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X