web-dev-qa-db-fra.com

Comment utiliser Any dans le type codable

Je travaille actuellement avec des types Codable dans mon projet et je suis confronté à un problème.

struct Person: Codable
{
    var id: Any
}

id dans le code ci-dessus pourrait être soit une String, soit une Int. C'est la raison pour laquelle id est de type Any.

Je sais que Any n'est pas Codable.

Ce que j'ai besoin de savoir, c'est comment puis-je le faire fonctionner.

14
PGDev

Codable a besoin de connaître le type de casting. 

Premièrement, j'essaierais d'aborder le problème de l'absence de type, de voir si vous pouvez résoudre ce problème et le rendre plus simple 

Sinon, la seule façon pour moi de résoudre votre problème consiste à utiliser des médicaments génériques, comme ci-dessous.

struct Person<T> {
    var id: T
    var name: String
}

let person1 = Person<Int>(id: 1, name: "John")
let person2 = Person<String>(id: "two", name: "Steve")
20
Scriptable

Valeur quantique

Tout d'abord, vous pouvez définir un type qui peut être décodé à la fois à partir d'une valeur String et Int . Le voici.

enum QuantumValue: Decodable {

    case int(Int), string(String)

    init(from decoder: Decoder) throws {
        if let int = try? decoder.singleValueContainer().decode(Int.self) {
            self = .int(int)
            return
        }

        if let string = try? decoder.singleValueContainer().decode(String.self) {
            self = .string(string)
            return
        }

        throw QuantumError.missingValue
    }

    enum QuantumError:Error {
        case missingValue
    }
}

La personne

Maintenant vous pouvez définir votre structure comme ceci

struct Person: Decodable {
    let id: QuantumValue
}

C'est tout. Testons-le!

JSON 1: id est String

let data = """
{
"id": "123"
}
""".data(using: String.Encoding.utf8)!

if let person = try? JSONDecoder().decode(Person.self, from: data) {
    print(person)
}

JSON 2: id est Int

let data = """
{
"id": 123
}
""".data(using: String.Encoding.utf8)!

if let person = try? JSONDecoder().decode(Person.self, from: data) {
    print(person)
}
17
Luca Angeletti

J'ai résolu ce problème en définissant une nouvelle structure décodable appelée AnyDecodable. Au lieu de Any, j'utilise AnyDecodable. Cela fonctionne parfaitement aussi avec les types imbriqués.

Essayez ceci dans un terrain de jeu:

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
    }
  ]
}
"""

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"))
    }
  }
}

let stud = try! JSONDecoder().decode(AnyDecodable.self, from: jsonData).value as! [String: Any]
print(stud)

Vous pouvez étendre ma structure à AnyCodable si vous êtes également intéressé par la partie Encodage.

Edit: je l'ai effectivement fait.

Voici AnyCodable

struct AnyCodable: Decodable {
  var value: Any

  struct CodingKeys: CodingKey {
    var stringValue: String
    var intValue: Int?
    init?(intValue: Int) {
      self.stringValue = "\(intValue)"
      self.intValue = intValue
    }
    init?(stringValue: String) { self.stringValue = stringValue }
  }

  init(value: Any) {
    self.value = value
  }

  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(AnyCodable.self, forKey: key).value
      }
      value = result
    } else if var container = try? decoder.unkeyedContainer() {
      var result = [Any]()
      while !container.isAtEnd {
        result.append(try container.decode(AnyCodable.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"))
    }
  }
}

extension AnyCodable: Encodable {
  func encode(to encoder: Encoder) throws {
    if let array = value as? [Any] {
      var container = encoder.unkeyedContainer()
      for value in array {
        let decodable = AnyCodable(value: value)
        try container.encode(decodable)
      }
    } else if let dictionary = value as? [String: Any] {
      var container = encoder.container(keyedBy: CodingKeys.self)
      for (key, value) in dictionary {
        let codingKey = CodingKeys(stringValue: key)!
        let decodable = AnyCodable(value: value)
        try container.encode(decodable, forKey: codingKey)
      }
    } else {
      var container = encoder.singleValueContainer()
      if let intVal = value as? Int {
        try container.encode(intVal)
      } else if let doubleVal = value as? Double {
        try container.encode(doubleVal)
      } else if let boolVal = value as? Bool {
        try container.encode(boolVal)
      } else if let stringVal = value as? String {
        try container.encode(stringVal)
      } else {
        throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
      }

    }
  }
}

Vous pouvez le tester avec le JSON précédent de cette manière dans un terrain de jeu:

let stud = try! JSONDecoder().decode(AnyCodable.self, from: jsonData)
print(stud.value as! [String: Any])

let backToJson = try! JSONEncoder().encode(stud)
let jsonString = String(bytes: backToJson, encoding: .utf8)!

print(jsonString)
9
Giuseppe Lanza

Si votre problème est qu'il est incertain du type d'identifiant car il peut s'agir d'une chaîne ou d'une valeur entière, je peux vous suggérer cet article de blog: http://agostini.tech/2017/11/12/Swift-4 -codable-in-real-life-part-2/

Fondamentalement, j'ai défini un nouveau type Decodable 

public struct UncertainValue<T: Decodable, U: Decodable>: Decodable {
    public var tValue: T?
    public var uValue: U?

    public var value: Any? {
        return tValue ?? uValue
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        tValue = try? container.decode(T.self)
        uValue = try? container.decode(U.self)
        if tValue == nil && uValue == nil {
            //Type mismatch
            throw DecodingError.typeMismatch(type(of: self), DecodingError.Context(codingPath: [], debugDescription: "The value is not of type \(T.self) and not even \(U.self)"))
        }

    }
}

A partir de maintenant, votre objet Personne serait

struct Person: Decodable {
    var id: UncertainValue<Int, String>
}

vous pourrez accéder à votre identifiant à l'aide de id.value

2
Giuseppe Lanza

Vous pouvez remplacer Any par une énumération acceptant une Int ou une String:

enum Id: Codable {
    case numeric(value: Int)
    case named(name: String)
}

struct Person: Codable
{
    var id: Id
}

Ensuite, le compilateur se plaindra du fait que Id n'est pas conforme à Decodable. Étant donné que Id a des valeurs associées, vous devez implémenter cela vous-même. Lisez https://littlebitesofcocoa.com/318-codable-enums pour obtenir un exemple de la procédure à suivre.

2
Clafou

Pour faire la clé comme Any , j'aime toutes les réponses ci-dessus. Mais lorsque vous ne savez pas quel type de données votre serveur vous enverra, vous utiliserez la classe Quantum (comme ci-dessus). Cependant, le type Quantum est un peu difficile à utiliser ou à gérer. Voici donc ma solution pour que votre clé de classe décodable soit un type de données quelconque (ou "id" pour les amateurs d'obj-c) 

   class StatusResp:Decodable{
    var success:Id? // Here i am not sure which datatype my server guy will send
}
enum Id: Decodable {

    case int(Int), double(Double), string(String) // Add more cases if you want

    init(from decoder: Decoder) throws {

        //Check each case
        if let dbl = try? decoder.singleValueContainer().decode(Double.self),dbl.truncatingRemainder(dividingBy: 1) != 0  { // It is double not a int value
            self = .double(dbl)
            return
        }

        if let int = try? decoder.singleValueContainer().decode(Int.self) {
            self = .int(int)
            return
        }
        if let string = try? decoder.singleValueContainer().decode(String.self) {
            self = .string(string)
            return
        }
        throw IdError.missingValue
    }

    enum IdError:Error { // If no case matched
        case missingValue
    }

    var any:Any{
        get{
            switch self {
            case .double(let value):
                return value
            case .int(let value):
                return value
            case .string(let value):
                return value
            }
        }
    }
}

Utilisation:  

let json = "{\"success\":\"hii\"}".data(using: .utf8) // response will be String
        //let json = "{\"success\":50.55}".data(using: .utf8)  //response will be Double
        //let json = "{\"success\":50}".data(using: .utf8) //response will be Int
        let decoded = try? JSONDecoder().decode(StatusResp.self, from: json!)
        print(decoded?.success) // It will print Any

        if let doubleValue = decoded?.success as? Double {

        }else if let doubleValue = decoded?.success as? Int {

        }else if let doubleValue = decoded?.success as? String {

        }
1
indrajit

Tout d’abord, comme vous pouvez le lire dans d’autres réponses et commentaires, utiliser Any pour cela n’est pas une bonne conception. Si possible, réfléchissez-y à deux fois.

Cela dit, si vous souhaitez vous y tenir pour vos propres raisons, vous devez écrire votre propre codage/décodage et adopter une sorte de convention dans le JSON sérialisé.

Le code ci-dessous l'implémente en encodant id toujours sous forme de chaîne et en décodant à Int ou String en fonction de la valeur trouvée.

import Foundation

struct Person: Codable {
    var id: Any

    init(id: Any) {
        self.id = id
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Keys.self)
        if let idstr = try container.decodeIfPresent(String.self, forKey: .id) {
            if let idnum = Int(idstr) {
                id = idnum
            }
            else {
                id = idstr
            }
            return
        }
        fatalError()
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: Keys.self)
        try container.encode(String(describing: id), forKey: .id)
    }

    enum Keys: String, CodingKey {
        case id
    }
}

extension Person: CustomStringConvertible {
    var description: String { return "<Person id:\(id)>" }
}

Exemples

Encoder un objet avec numérique id:

var p1 = Person(id: 1)
print(String(data: try JSONEncoder().encode(p1), 
      encoding: String.Encoding.utf8) ?? "/* ERROR */")
// {"id":"1"}

Encoder un objet avec la chaîne id:

var p2 = Person(id: "root")
print(String(data: try JSONEncoder().encode(p2), 
      encoding: String.Encoding.utf8) ?? "/* ERROR */")
// {"id":"root"}

Décoder en numérique id:

print(try JSONDecoder().decode(Person.self, 
      from: "{\"id\": \"2\"}".data(using: String.Encoding.utf8)!))
// <Person id:2>

Décoder en chaîne id:

print(try JSONDecoder().decode(Person.self, 
      from: "{\"id\": \"admin\"}".data(using: String.Encoding.utf8)!))
// <Person id:admin>

Une autre implémentation consisterait à coder en Int ou String et à envelopper les tentatives de décodage dans un do...catch

Dans la partie encodage:

    if let idstr = id as? String {
        try container.encode(idstr, forKey: .id)
    }
    else if let idnum = id as? Int {
        try container.encode(idnum, forKey: .id)
    }

Et puis décoder le bon type en plusieurs tentatives:

do {
    if let idstr = try container.decodeIfPresent(String.self, forKey: .id) {
        id = idstr
        id_decoded = true
    }
}
catch {
    /* pass */
}

if !id_decoded {
    do {
        if let idnum = try container.decodeIfPresent(Int.self, forKey: .id) {
            id = idnum
        }
    }
    catch {
        /* pass */
    }
}

C'est plus laid à mon avis.

En fonction du contrôle que vous avez sur la sérialisation du serveur, vous pouvez utiliser l'un ou l'autre ou écrire quelque chose d'autre, adapté à la sérialisation réelle.

1
djromero

Ici votre id peut être n’importe quel type Codable:

Swift 4.2

struct Person<T: Codable>: Codable {

    var id: T
    var name: String?
}

let p1 = Person(id: 1, name: "Bill")
let p2 = Person(id: "one", name: "John")
0
Mad Man

Vous pouvez simplement utiliser le type AnyCodable de la bibliothèque cool de Matt Thompson AnyCodable .

Par exemple:

import AnyCodable

struct Person: Codable
{
    var id: AnyCodable
}
0
Johnykutty

Il y a un cas d'angle qui n'est pas couvert par la solution de Luca Angeletti.

Par exemple, si le type de Cordinate est Double ou [Double], la solution d'Angeletti provoquera une erreur: "On s'attend à décoder Double mais à la place un tableau"

Dans ce cas, vous devez utiliser enum imbriqué dans Cordinate.

enum Cordinate: Decodable {
    case double(Double), array([Cordinate])

    init(from decoder: Decoder) throws {
        if let double = try? decoder.singleValueContainer().decode(Double.self) {
            self = .double(double)
            return
        }

        if let array = try? decoder.singleValueContainer().decode([Cordinate].self) {
            self = .array(array)
            return
        }

        throw CordinateError.missingValue
    }

    enum CordinateError: Error {
        case missingValue
    }
}

struct Geometry : Decodable {
    let date : String?
    let type : String?
    let coordinates : [Cordinate]?

    enum CodingKeys: String, CodingKey {

        case date = "date"
        case type = "type"
        case coordinates = "coordinates"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        date = try values.decodeIfPresent(String.self, forKey: .date)
        type = try values.decodeIfPresent(String.self, forKey: .type)
        coordinates = try values.decodeIfPresent([Cordinate].self, forKey: .coordinates)
    }
}
0
UnchartedWorks