web-dev-qa-db-fra.com

Comment décoder une propriété de type dictionnaire JSON dans le protocole décodable Swift 4

Disons que j'ai un type de données Customer qui contient une propriété metadata pouvant 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": "[email protected]",
  "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 convertir la propriété à partir d'un JSON désérialisé de NSJSONDeserialization, mais avec le nouveau protocole Swift 4 Decodable, je ne vois toujours pas comment y parvenir.

Est-ce que quelqu'un sait comment y parvenir dans Swift 4 avec le protocole Decodable?

Inspirée de ce Gist j’ai trouvé, j’ai écrit quelques extensions pour UnkeyedDecodingContainer et KeyedDecodingContainer. Vous pouvez trouver un lien vers mon Gist ici . En utilisant ce code, vous pouvez maintenant décoder tout Array<Any> ou Dictionary<String, Any> avec la syntaxe habituelle:

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 un avertissement que j'ai trouvé qui décode un tableau de dictionnaires [[String: Any]] La syntaxe requise est la suivante. Vous voudrez probablement jeter 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 voulez simplement convertir un fichier entier en dictionnaire, mieux vaut vous en tenir à l'api de JSONSerialization car je n'ai pas trouvé le moyen d'étendre JSONDecoder 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)
    }
}
45
loudmouth

J'ai aussi joué avec ce problème et j'ai finalement écrit une bibliothèque simple pour travailler avec les types «génériques 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.

12
zoul

Lorsque j’ai trouvé l’ancienne réponse, j’ai seulement testé un cas d’objet JSON simple, mais non vide, ce qui provoquerait une exception d’exécution telle que @slurmomatic et @zoul found. Désolé pour ce problème.

J'essaie donc d'une autre manière en utilisant un protocole JSONValue simple, en implémentant la structure AnyJSONValue de type erasure et en utilisant ce type à la place 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

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 devons attendre que Conditional Conformance atterrisse à Swift pour résoudre ce problème ou au moins lui permettre d’être mieux.


[Ancienne réponse]

Je poste cette question sur le forum Apple Developer 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 mon mal 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 où Any pourrait ê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)
5
Giuseppe Lanza

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": "[email protected]",
    "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
Suhit Patil

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
canius

La méthode la plus simple et la plus conseillée consiste à créer un modèle distinct pour chaque dictionnaire ou modèle en 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))

    }
}

_ {Usage:

  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é facultatif pour être en sécurité lors de l'analyse, peut être modifié au besoin.

En savoir plus sur ce sujet

0
minhazur

extension ViewController {func swiftyJson () {

    let url = URL(string: "https://iTunes.Apple.com/search?term=jack+johnson")
    //let url = URL(string: "http://makani.bitstaging.in/api/business/businesses_list")

    Alamofire.request(url!, method: .get, parameters: nil).responseJSON { response in
        var arrayIndexes = [IndexPath]()
        switch(response.result) {
        case .success(_):

            let data = response.result.value as! [String : Any]

            if let responseData =  Mapper<DataModel>().map(JSON: data) {
                if responseData.results!.count > 0{
                    self.arrayExploreStylistList = []
                }
                for i in 0..<responseData.results!.count{
                    arrayIndexes.append(IndexPath(row: self.arrayExploreStylistList.count + i, section: 0))
                }
                self.arrayExploreStylistList.append(contentsOf: responseData.results!)

                print(arrayIndexes.count)

            }

            //                    if let arrNew = data["results"] as? [[String : Any]]{
            //                        let jobData = Mapper<DataModel>().mapArray(JSONArray: arrNew)
            //                        print(jobData)
            //                        self.datamodel = jobData
            //                    }
            self.tblView.reloadData()
            break

        case .failure(_):
            print(response.result.error as Any)
            break

        }
    }

}

}

0
hiren

Voici une approche plus générique (pas seulement [String: Any], mais [Any] peut être décodé) et encapsulée (une entité distincte est utilisée pour cela) inspirée par la réponse @loudmouth.

Son utilisation 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 encapsuler le décodage de données JSON en objet JSON (tableau ou dictionnaire) sans étendre *DecodingContainer (pour ne pas interférer avec les rares cas où un objet JSON n'est pas désigné par [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 sauvegardés par NSNumber, sinon quelque chose comme ça ne marchera pas:

if customer.metadata["keyForInt"] as? Int64 { // as it always will be nil
0

J'ai créé un pod pour faciliter la manière dont le décodage + encodage [String: Any], [Any]. Et cela permet d’encoder ou de décoder les propriétés facultatives, ici https://github.com/levantAJ/AnyCodable

pod 'DynamicCodable', '1.0'

Comment l'utiliser:

import DynamicCodable

struct YourObject: Codable {
    var dict: [String: Any]
    var array: [Any]
    var optionalDict: [String: Any]?
    var optionalArray: [Any]?

    enum CodingKeys: String, CodingKey {
        case dict
        case array
        case optionalDict
        case optionalArray
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        dict = try values.decode([String: Any].self, forKey: .dict)
        array = try values.decode([Any].self, forKey: .array)
        optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
        optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(dict, forKey: .dict)
        try container.encode(array, forKey: .array)
        try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
        try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
    }
}
0
Tai Le

Si vous utilisez SwiftyJSON pour analyser JSON, vous pouvez effectuer une mise à jour vers 4.1.0 qui prend en charge le protocole Codable. Il suffit de déclarer metadata: JSON et vous êtes tous ensemble.

import SwiftyJSON

struct Customer {
  let id: String
  let email: String
  let metadata: JSON
}
0
allen huang