138 votes

Comment décoder une propriété avec le type de dictionnaire JSON en Swift [45] protocole décodable

Disons que j'ai Customer qui contient un metadata qui peut contenir tout 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
  }
}

El metadata peut être n'importe quel objet arbitraire de la carte JSON.

Avant de pouvoir définir la propriété à partir d'un JSON désérialisé à partir de NSJSONDeserialization mais avec le nouveau Swift 4 Decodable protocole, je n'arrive toujours pas à trouver un moyen de le faire.

Quelqu'un sait-il comment réaliser cela dans Swift 4 avec le protocole Decodable ?

113voto

loudmouth Points 981

En s'inspirant de cette phrase J'ai trouvé, j'ai écrit quelques extensions pour UnkeyedDecodingContainer y KeyedDecodingContainer . Vous pouvez trouver un lien vers mon gist aquí . En utilisant ce code, vous pouvez maintenant décoder n'importe quel Array<Any> o Dictionary<String, Any> avec la syntaxe familière :

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)

o

let array: [Any] = try container.decode([Any].self, forKey: key)

Edit : il y a un problème que j'ai trouvé 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 forcer le casting :

let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]

EDIT 2 : Si vous souhaitez simplement convertir un fichier entier en un dictionnaire, il est préférable de s'en tenir à l'api de JSONSerialization, car je n'ai pas trouvé le 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)
    }
}

0 votes

Intéressant, je vais essayer ce gist et je vous informerai du résultat @loudmouth.

0 votes

@PitiphongPhongpattranont ce code a-t-il fonctionné pour vous ?

0 votes

Je dirais que oui. J'ai un peu remanié cet extrait mais votre idée principale fonctionne très bien. Merci.

29voto

zoul Points 51637

J'ai aussi joué avec ce problème, et j'ai finalement écrit une bibliothèque simple pour travailler avec les types "JSON génériques". . (Où "générique" signifie "sans structure connue à l'avance".) Le point principal est de représenter 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 ensuite mettre en œuvre Codable y Equatable .

16voto

Suhit Patil Points 6334

Vous pouvez créer une structure de métadonnées qui confirme à Decodable protocole et utilisation JSONDecoder pour créer un objet à partir de données en utilisant la méthode de décodage 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: Decodable {
    let object: String
    let id: String
    let email: String
    let metadata: Metadata
}

struct Metadata: Decodable {
    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)
}

20 votes

Non je ne peux pas, puisque je ne connais pas la structure de la metadata valeur. Il peut s'agir de n'importe quel objet arbitraire.

0 votes

Voulez-vous dire qu'il peut être de type tableau ou dictionnaire ?

0 votes

Pouvez-vous donner un exemple ou ajouter plus d'explications sur la structure des métadonnées ?

9voto

Giuseppe Lanza Points 1929

J'ai trouvé une solution légèrement différente.

Supposons que nous ayons quelque chose de plus qu'un simple [String: Any] à analyser étaient Tout peut être un tableau, un dictionnaire imbriqué ou un dictionnaire de tableaux.

Quelque chose comme ça :

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, voici 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)

8voto

Lorsque j'ai trouvé l'ancienne réponse, je n'ai testé qu'un cas d'objet JSON simple mais pas un cas vide qui causerait une exception d'exécution comme @slurmomatic et @zoul l'ont trouvé. Désolé pour ce problème.

J'ai donc essayé une autre solution en utilisant un simple protocole JSONValue, en implémentant la fonction AnyJSONValue et utiliser ce type de structure d'effacement au lieu de Any . Voici une mise en œuvre.

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.

metadata = try container.decode ([String: AnyJSONValue].self, forKey: .metadata)

Le problème avec cette question est que nous devons appeler value.jsonValue as? Int . Nous devons attendre jusqu'à ce que Conditional Conformance dans Swift, cela résoudrait ce problème ou du moins l'aiderait à être meilleur.


[Ancienne réponse]

Je pose cette question sur le forum des développeurs Apple et il s'avère que c'est très facile.

Je peux le faire.

metadata = try container.decode ([String: Any].self, forKey: .metadata)

dans l'initialisateur.

C'est ma faute si j'ai manqué ça en premier lieu.

4 votes

Pourriez-vous poster le lien vers la question sur Apple Developer. Any n'est pas conforme à Decodable donc je ne suis pas sûr que ce soit la bonne réponse.

0 votes

@RezaShirazian C'est ce que je pensais en premier lieu. Mais il s'avère que Dictionary est conforme à Encodable lorsque ses clés sont conformes à Hashable et ne dépendent pas de ses valeurs. Vous pouvez ouvrir l'en-tête de Dictionary et voir cela par vous-même. extension Dictionary : Encodable where Key : Hashable extension Dictionary : Decodable where Key : Hashable forums.developer.apple.com/thread/80288#237680

7 votes

Actuellement, cela ne fonctionne pas. "Dictionary<String, Any> n'est pas conforme à Decodable car Any n'est pas conforme à Decodable"

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