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) ".
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 Dictionary
Codable
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")))
Pour résoudre votre problème, vous pouvez utiliser l'un des deux extraits de code Playground suivants.
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"
*/
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"
*/