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?

54
demandé sur Pitiphong Phongpattranont 2017-06-17 13:03:26

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)
    }
}
36
répondu loudmouth 2018-10-02 19:17:07

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.

10
répondu zoul 2018-03-02 14:47:03

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.

6
répondu Pitiphong Phongpattranont 2017-08-26 08:58:28

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)
5
répondu Giuseppe Lanza 2018-01-12 13:00:40

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)
}
4
répondu Suhit Patil 2017-06-17 11:57:17

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)!)
1
répondu canius 2017-10-28 13:42:57

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.

En Lire plus sur ce sujet

0
répondu minhazur 2018-01-01 06:39:17

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
0
répondu Aleksey Kozhevnikov 2018-01-22 16:55:10

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
}
0
répondu allen huang 2018-04-26 05:34:21

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)
-1
répondu Code Different 2017-06-19 02:55:30