web-dev-qa-db-fra.com

Swift 4 Decodable - Dictionnaire avec enum comme clé

Ma structure de données a une énumération comme clé, je m'attends à ce que ce qui suit se décode automatiquement. Est-ce un bug ou un problème de configuration?

import Foundation

enum AnEnum: String, Codable {
  case enumValue
}

struct AStruct: Codable {
  let dictionary: [AnEnum: String]
}

let jsonDict = ["dictionary": ["enumValue": "someString"]]
let data = try! JSONSerialization.data(withJSONObject: jsonDict,     options: .prettyPrinted)
let decoder = JSONDecoder()
do {
  try decoder.decode(AStruct.self, from: data)
} catch {
  print(error)
}

L'erreur que j'obtiens est la suivante, semble confondre le dict avec un tableau.

typeMismatch (Swift.Array, Swift.DecodingError.Context (codingPath: [Facultatif (__ lldb_expr_85.AStruct. (CodingKeys in _0E2FD0A9B523101D0DCD67578F72D1DD) .dictionary)], debugDescription à la place de "ray) ".

24
Chris Mitchelmore

Le problème est que la conformité Dictionary de Codable ne peut actuellement gérer correctement que les clés String et Int. Pour un dictionnaire avec tout autre type Key (où Key est Encodable/Decodable), il est codé et décodé avec un conteneur sans clé (tableau JSON) avec des valeurs de clé alternées.

Par conséquent, lorsque vous tentez de décoder le JSON:

{"dictionary": {"enumValue": "someString"}}

dans AStruct, la valeur de la clé "dictionary" devrait être un tableau.

Alors,

let jsonDict = ["dictionary": ["enumValue", "someString"]]

fonctionnerait, donnant le JSON:

{"dictionary": ["enumValue", "someString"]}

qui serait ensuite décodé en:

AStruct(dictionary: [AnEnum.enumValue: "someString"])

Cependant, je pense vraiment que la conformité de DictionaryCodable devrait être capable de gérer correctement tout CodingKey comme son Key (qui peut être AnEnum) - car il peut simplement encoder et décoder dans un conteneur à clé avec cette clé (n'hésitez pas à déposer un bug demandant cela).

Jusqu'à ce qu'il soit implémenté (le cas échéant), nous pourrions toujours créer un type d'encapsuleur pour ce faire:

struct CodableDictionary<Key : Hashable, Value : Codable> : Codable where Key : CodingKey {

    let decoded: [Key: Value]

    init(_ decoded: [Key: Value]) {
        self.decoded = decoded
    }

    init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: Key.self)

        decoded = Dictionary(uniqueKeysWithValues:
            try container.allKeys.lazy.map {
                (key: $0, value: try container.decode(Value.self, forKey: $0))
            }
        )
    }

    func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: Key.self)

        for (key, value) in decoded {
            try container.encode(value, forKey: key)
        }
    }
}

puis implémenter comme suit:

enum AnEnum : String, CodingKey {
    case enumValue
}

struct AStruct: Codable {

    let dictionary: [AnEnum: String]

    private enum CodingKeys : CodingKey {
        case dictionary
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        dictionary = try container.decode(CodableDictionary.self, forKey: .dictionary).decoded
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(CodableDictionary(dictionary), forKey: .dictionary)
    }
}

(ou ayez simplement la propriété dictionary de type CodableDictionary<AnEnum, String> et utilisez la conformité Codable générée automatiquement - puis parlez simplement en termes de dictionary.decoded)

Maintenant, nous pouvons décoder l'objet JSON imbriqué comme prévu:

let data = """
{"dictionary": {"enumValue": "someString"}}
""".data(using: .utf8)!

let decoder = JSONDecoder()
do {
    let result = try decoder.decode(AStruct.self, from: data)
    print(result)
} catch {
    print(error)
}

// AStruct(dictionary: [AnEnum.enumValue: "someString"])

Bien que tout cela soit dit, on pourrait faire valoir que tout ce que vous réalisez avec un dictionnaire avec un enum comme clé est juste un struct avec des propriétés facultatives (et si vous attendez une valeur donnée pour être toujours là, le rendre non facultatif).

Par conséquent, vous souhaiterez peut-être que votre modèle ressemble à:

struct BStruct : Codable {
    var enumValue: String?
}

struct AStruct: Codable {

    private enum CodingKeys : String, CodingKey {
        case bStruct = "dictionary"
    }

    let bStruct: BStruct
}

Ce qui fonctionnerait très bien avec votre JSON actuel:

let data = """
{"dictionary": {"enumValue": "someString"}}
""".data(using: .utf8)!

let decoder = JSONDecoder()
do {
    let result = try decoder.decode(AStruct.self, from: data)
    print(result)
} catch {
    print(error)
}

// AStruct(bStruct: BStruct(enumValue: Optional("someString")))
24
Hamish

Pour résoudre votre problème, vous pouvez utiliser l'un des deux extraits de code Playground suivants.


#1. Utilisation de l'initialiseur init(from:) de Decodable

import Foundation

enum AnEnum: String, Codable {
    case enumValue
}

struct AStruct {
    enum CodingKeys: String, CodingKey {
        case dictionary
    }
    enum EnumKeys: String, CodingKey {
        case enumValue
    }

    let dictionary: [AnEnum: String]
}

extension AStruct: Decodable {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let dictContainer = try container.nestedContainer(keyedBy: EnumKeys.self, forKey: .dictionary)

        var dictionary = [AnEnum: String]()
        for enumKey in dictContainer.allKeys {
            guard let anEnum = AnEnum(rawValue: enumKey.rawValue) else {
                let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to an AnEnum object")
                throw DecodingError.dataCorrupted(context)
            }
            let value = try dictContainer.decode(String.self, forKey: enumKey)
            dictionary[anEnum] = value
        }
        self.dictionary = dictionary
    }

}

tilisation:

let jsonString = """
{
  "dictionary" : {
    "enumValue" : "someString"
  }
}
"""

let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let aStruct = try! decoder.decode(AStruct.self, from: data)
dump(aStruct)

/*
 prints:
 ▿ __lldb_expr_148.AStruct
   ▿ dictionary: 1 key/value pair
     ▿ (2 elements)
       - key: __lldb_expr_148.AnEnum.enumValue
       - value: "someString"
 */

# 2. Utilisation de la méthode decode(_:forKey:) de KeyedDecodingContainerProtocol

import Foundation

public enum AnEnum: String, Codable {
    case enumValue
}

struct AStruct: Decodable {
    enum CodingKeys: String, CodingKey {
        case dictionary
    }

    let dictionary: [AnEnum: String]
}

public extension KeyedDecodingContainer  {

    public func decode(_ type: [AnEnum: String].Type, forKey key: Key) throws -> [AnEnum: String] {
        let stringDictionary = try self.decode([String: String].self, forKey: key)
        var dictionary = [AnEnum: String]()

        for (key, value) in stringDictionary {
            guard let anEnum = AnEnum(rawValue: key) else {
                let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to an AnEnum object")
                throw DecodingError.dataCorrupted(context)
            }
            dictionary[anEnum] = value
        }

        return dictionary
    }

}

tilisation:

let jsonString = """
{
  "dictionary" : {
    "enumValue" : "someString"
  }
}
"""

let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let aStruct = try! decoder.decode(AStruct.self, from: data)
dump(aStruct)

/*
 prints:
 ▿ __lldb_expr_148.AStruct
   ▿ dictionary: 1 key/value pair
     ▿ (2 elements)
       - key: __lldb_expr_148.AnEnum.enumValue
       - value: "someString"
 */
3
Imanou Petit