Comment décoder une propriété avec le type de dictionnaire JSON dans le protocole décodable Swift 4
Disons que j'ai Customer
Type de données qui contient une propriété metadata
qui peut contenir n'importe quel dictionnaire JSON dans l'objet client
struct Customer {
let id: String
let email: String
let metadata: [String: Any]
}
{
"object": "customer",
"id": "4yq6txdpfadhbaqnwp3",
"email": "john.doe@example.com",
"metadata": {
"link_id": "linked-id",
"buy_count": 4
}
}
La propriété metadata
peut être n'importe quel objet de carte JSON arbitraire.
Avant de pouvoir lancer la propriété à partir d'un JSON désérialisé à partir de NSJSONDeserialization
mais avec le nouveau protocole Swift 4 Decodable
, Je ne peux toujours pas penser à un moyen de le faire.
Est-ce que quelqu'un sait comment y parvenir dans Swift 4 avec le protocole décodable?
10 réponses
Avec un peu d'inspiration de cet essentiel que j'ai trouvé, j'ai écrit quelques extensions pour UnkeyedDecodingContainer
et KeyedDecodingContainer
. Vous pouvez trouver un lien vers mon essentiel ici . En utilisant ce code vous pouvez maintenant décoder tout Array<Any>
ou Dictionary<String, Any>
, avec la syntaxe familière:
let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)
Ou
let array: [Any] = try container.decode([Any].self, forKey: key)
Edit: Il y a une mise en garde que j'ai trouvée qui est le décodage d'un tableau de dictionnaires [[String: Any]]
la syntaxe requise est la suivante. Vous voudrez probablement lancer une erreur au lieu de la force casting:
let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]
EDIT 2: Si vous voulez simplement convertir un fichier entier en Dictionnaire, il vaut mieux coller avec l'api de JSONSerialization car je n'ai pas trouvé un moyen d'étendre JSONDecoder lui-même pour décoder directement un dictionnaire.
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
// appropriate error handling
return
}
Les extensions
// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a
struct JSONCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
// See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
if try decodeNil() {
continue
} else if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
J'ai aussi joué avec ce problème, et j'ai finalement écrit une bibliothèque simple pour travailler avec les types "generic JSON" . (Où "Générique" signifie "sans structure connue à l'avance".) Le point principal représente le JSON générique avec un type concret:
public enum JSON {
case string(String)
case number(Float)
case object([String:JSON])
case array([JSON])
case bool(Bool)
case null
}
Ce type peut alors implémenter Codable
et Equatable
.
Quand j'ai trouvé l'ancienne réponse, j'ai seulement testé un cas d'objet JSON simple mais pas un cas vide qui provoquera une exception d'exécution comme @slurmomatic et @zoul found. Désolé pour cette question.
J'essaie donc une autre façon en ayant un simple protocole JSONValue, implémentez la structure d'effacement de type AnyJSONValue
et utilisez ce type au lieu de Any
. Voici une implémentation.
public protocol JSONType: Decodable {
var jsonValue: Any { get }
}
extension Int: JSONType {
public var jsonValue: Any { return self }
}
extension String: JSONType {
public var jsonValue: Any { return self }
}
extension Double: JSONType {
public var jsonValue: Any { return self }
}
extension Bool: JSONType {
public var jsonValue: Any { return self }
}
public struct AnyJSONType: JSONType {
public let jsonValue: Any
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intValue = try? container.decode(Int.self) {
jsonValue = intValue
} else if let stringValue = try? container.decode(String.self) {
jsonValue = stringValue
} else if let boolValue = try? container.decode(Bool.self) {
jsonValue = boolValue
} else if let doubleValue = try? container.decode(Double.self) {
jsonValue = doubleValue
} else if let doubleValue = try? container.decode(Array<AnyJSONType>.self) {
jsonValue = doubleValue
} else if let doubleValue = try? container.decode(Dictionary<String, AnyJSONType>.self) {
jsonValue = doubleValue
} else {
throw DecodingError.typeMismatch(JSONType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported JSON tyep"))
}
}
}
Et voici comment l'utiliser lors du décodage de
metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)
Le problème avec ce problème est que nous devons appeler value.jsonValue as? Int
. Nous il faut attendre que Conditional Conformance
atterrisse à Swift, ce qui résoudrait ce problème ou au moins l'aiderait à être meilleur.
[Ancienne Réponse]
Je poste cette question sur le Forum des développeurs D'Apple et il s'avère que c'est très facile.
Je peux faire
metadata = try container.decode ([String: Any].self, forKey: .metadata)
Dans l'initialiseur.
C'était ma faute de rater ça en premier lieu.
Je suis venu avec une solution légèrement différente.
Supposons que nous ayons quelque chose de plus qu'un simple [String: Any]
à analyser si l'un peut être un tableau ou un dictionnaire imbriqué ou un dictionnaire de tableaux.
Quelque Chose comme ceci:
var json = """
{
"id": 12345,
"name": "Giuseppe",
"last_name": "Lanza",
"age": 31,
"happy": true,
"rate": 1.5,
"classes": ["maths", "phisics"],
"dogs": [
{
"name": "Gala",
"age": 1
}, {
"name": "Aria",
"age": 3
}
]
}
"""
Eh Bien, c'est ma solution:
public struct AnyDecodable: Decodable {
public var value: Any
private struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}
public init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyDecodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyDecodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}
Essayez-le en utilisant
let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
print(stud)
Vous pouvez créer une structure de métadonnées qui confirme le protocole Codable
et utiliser la classe Decodable
pour créer un objet comme ci-dessous
let json: [String: Any] = [
"object": "customer",
"id": "4yq6txdpfadhbaqnwp3",
"email": "john.doe@example.com",
"metadata": [
"link_id": "linked-id",
"buy_count": 4
]
]
struct Customer: Codable {
let object: String
let id: String
let email: String
let metadata: Metadata
}
struct Metadata: Codable {
let link_id: String
let buy_count: Int
}
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
let decoder = JSONDecoder()
do {
let customer = try decoder.decode(Customer.self, from: data)
print(customer)
} catch {
print(error.localizedDescription)
}
Vous pourriez jeter un oeil à BeyovaJSON
import BeyovaJSON
struct Customer: Codable {
let id: String
let email: String
let metadata: JToken
}
//create a customer instance
customer.metadata = ["link_id": "linked-id","buy_count": 4]
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
print(String(bytes: try! encoder.encode(customer), encoding: .utf8)!)
Le moyen le plus simple et suggéré est de Créer un modèle séparé pour chaque dictionnaire ou modèle qui est dans JSON .
Voici ce que je fais
//Model for dictionary **Metadata**
struct Metadata: Codable {
var link_id: String?
var buy_count: Int?
}
//Model for dictionary **Customer**
struct Customer: Codable {
var object: String?
var id: String?
var email: String?
var metadata: Metadata?
}
//Here is our decodable parser that decodes JSON into expected model
struct CustomerParser {
var customer: Customer?
}
extension CustomerParser: Decodable {
//keys that matches exactly with JSON
enum CustomerKeys: String, CodingKey {
case object = "object"
case id = "id"
case email = "email"
case metadata = "metadata"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CustomerKeys.self) // defining our (keyed) container
let object: String = try container.decode(String.self, forKey: .object) // extracting the data
let id: String = try container.decode(String.self, forKey: .id) // extracting the data
let email: String = try container.decode(String.self, forKey: .email) // extracting the data
//Here I have used metadata model instead of dictionary [String: Any]
let metadata: Metadata = try container.decode(Metadata.self, forKey: .metadata) // extracting the data
self.init(customer: Customer(object: object, id: id, email: email, metadata: metadata))
}
}
Utilisation:
if let url = Bundle.main.url(forResource: "customer-json-file", withExtension: "json") {
do {
let jsonData: Data = try Data(contentsOf: url)
let parser: CustomerParser = try JSONDecoder().decode(CustomerParser.self, from: jsonData)
print(parser.customer ?? "null")
} catch {
}
}
* * j'ai utilisé optionnel pour être en sécurité lors de l'analyse, peut être modifié au besoin.
Voici une approche plus générique (non seulement [String: Any]
, mais [Any]
peut être décodée) et encapsulée (une entité séparée est utilisée pour cela) inspirée par la réponse @loudmouth.
L'utiliser ressemblera à:
extension Customer: Decodable {
public init(from decoder: Decoder) throws {
let selfContainer = try decoder.container(keyedBy: CodingKeys.self)
id = try selfContainer.decode(.id)
email = try selfContainer.decode(.email)
let metadataContainer: JsonContainer = try selfContainer.decode(.metadata)
guard let metadata = metadataContainer.value as? [String: Any] else {
let context = DecodingError.Context(codingPath: [CodingKeys.metadata], debugDescription: "Expected '[String: Any]' for 'metadata' key")
throw DecodingError.typeMismatch([String: Any].self, context)
}
self.metadata = metadata
}
private enum CodingKeys: String, CodingKey {
case id, email, metadata
}
}
JsonContainer
est une entité d'assistance que nous utilisons pour envelopper les données JSON de décodage dans un objet JSON (tableau ou Dictionnaire) sans étendre *DecodingContainer
(de sorte qu'il n'interférera pas avec de rares cas où un objet JSON n'est pas destiné à [String: Any]
).
struct JsonContainer {
let value: Any
}
extension JsonContainer: Decodable {
public init(from decoder: Decoder) throws {
if let keyedContainer = try? decoder.container(keyedBy: Key.self) {
var dictionary = [String: Any]()
for key in keyedContainer.allKeys {
if let value = try? keyedContainer.decode(Bool.self, forKey: key) {
// Wrapping numeric and boolean types in `NSNumber` is important, so `as? Int64` or `as? Float` casts will work
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(Int64.self, forKey: key) {
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(Double.self, forKey: key) {
dictionary[key.stringValue] = NSNumber(value: value)
} else if let value = try? keyedContainer.decode(String.self, forKey: key) {
dictionary[key.stringValue] = value
} else if (try? keyedContainer.decodeNil(forKey: key)) ?? false {
// NOP
} else if let value = try? keyedContainer.decode(JsonContainer.self, forKey: key) {
dictionary[key.stringValue] = value.value
} else {
throw DecodingError.dataCorruptedError(forKey: key, in: keyedContainer, debugDescription: "Unexpected value for \(key.stringValue) key")
}
}
value = dictionary
} else if var unkeyedContainer = try? decoder.unkeyedContainer() {
var array = [Any]()
while !unkeyedContainer.isAtEnd {
let container = try unkeyedContainer.decode(JsonContainer.self)
array.append(container.value)
}
value = array
} else if let singleValueContainer = try? decoder.singleValueContainer() {
if let value = try? singleValueContainer.decode(Bool.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(Int64.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(Double.self) {
self.value = NSNumber(value: value)
} else if let value = try? singleValueContainer.decode(String.self) {
self.value = value
} else if singleValueContainer.decodeNil() {
value = NSNull()
} else {
throw DecodingError.dataCorruptedError(in: singleValueContainer, debugDescription: "Unexpected value")
}
} else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Invalid data format for JSON")
throw DecodingError.dataCorrupted(context)
}
}
private struct Key: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
}
Notez que les types numberic et boolean sont soutenus par NSNumber
, sinon quelque chose comme ça ne fonctionnera pas:
if customer.metadata["keyForInt"] as? Int64 { // as it always will be nil
Si vous utilisez SwiftyJSON pour analyser JSON, vous pouvez mettre à jour vers 4.1.0 qui a un support de protocole Codable
. Il suffit de déclarer metadata: JSON
et vous êtes prêt.
import SwiftyJSON
struct Customer {
let id: String
let email: String
let metadata: JSON
}
Ce que vous voulez va à l'encontre de la conception de Codable
. L'idée derrière Codable
est de fournir un mécanisme pour archiver et désarchiver les données d'une manière sûre. Cela signifie que vous devez définir les propriétés et leurs types de données avant la main. Je peux penser à 2 solutions à votre problème:
1. Lister toutes les clés de métadonnées potentielles
Souvent, si vous accédez suffisamment à la documentation de L'API, vous trouverez la liste complète de toutes les clés de métadonnées potentielles. Définissez une structure Metadata
, avec ces clés comme propriétés optionnelles:
struct Customer: Decodable {
struct Metadata: Decodable {
var linkId: String?
var buyCount: Int?
var somethingElse: Int?
private enum CodingKeys: String, CodingKey {
case linkId = "link_id"
case buyCount = "buy_count"
case somethingElse = "something_else"
}
}
var object: String
var id: String
var email: String
var metadata: Metadata
}
let customer = try! JSONDecoder().decode(Customer.self, from: jsonData)
print(customer.metadata)
Je peux voir que les concepteurs Swift auraient préféré cette approche.
2. Combiner décodable et JSONSerialization
JSONSerialization
offre un grand dynamisme dans le compromis pour la sécurité de type. Vous pouvez certainement le mélanger avec Decodable
, dont la philosophie de conception est tout le contraire:
struct Customer {
private struct RawCustomer: Decodable {
var object: String
var id: String
var email: String
}
var object: String
var id: String
var email: String
var metadata: [String: AnyObject]
init(jsonData: Data) throws {
let rawCustomer = try JSONDecoder().decode(RawCustomer.self, from: jsonData)
object = rawCustomer.object
id = rawCustomer.id
email = rawCustomer.email
let jsonObject = try JSONSerialization.jsonObject(with: jsonData)
if let dict = jsonObject as? [String: AnyObject],
let metadata = dict["metadata"] as? [String: AnyObject]
{
self.metadata = metadata
} else {
self.metadata = [String: AnyObject]()
}
}
}
let customer = try! Customer(jsonData: jsonData)
print(customer.metadata)