web-dev-qa-db-fra.com

Utiliser codable avec une clé qui est parfois un Int et parfois une String

J'ai une API qui retournera parfois une clé spécifique (dans ce cas id) dans le JSON en tant qu'int et d'autres fois, elle renverra cette même clé en tant que chaîne. Comment utiliser codable pour analyser ce JSON?

struct GeneralProduct: Codable {
    var price:Double!
    var id:String?
    var name:String!

    private enum CodingKeys: String, CodingKey {
        case price = "p"
        case id = "i"
        case name = "n"
    }

    init(price:Double? = nil, id: String? = nil, name: String? = nil) {
        self.price = price
        self.id = id
        self.name = name
    }
}

Je reçois toujours ce message d'erreur: Expected to decode String but found a number instead. La raison pour laquelle il retourne un nombre est parce que le champ id est vide et lorsque le champ id est vide, il revient par défaut à retourner 0 comme ID que le codable identifie comme un nombre. Je peux essentiellement ignorer la clé d'identification mais codable ne me donne pas la possibilité de l'ignorer à ma connaissance. Quelle serait la meilleure façon de gérer cela?

Voici le JSON. C'est super simple

Travail

{
  "p":2.12,
  "i":"3k3mkfnk3",
  "n":"Blue Shirt"
}

Erreur - car il n'y a pas d'id dans le système, il retourne 0 par défaut que codable voit évidemment comme un nombre opposé à une chaîne.

{
  "p":2.19,
  "i":0,
  "n":"Black Shirt"
}
22
Nevin Jethmalani
struct GeneralProduct: Codable {
    var price: Double?
    var id: String?
    var name: String?
    private enum CodingKeys: String, CodingKey {
        case price = "p", id = "i", name = "n"
    }
    init(price: Double? = nil, id: String? = nil, name: String? = nil) {
        self.price = price
        self.id = id
        self.name = name
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        price = try container.decode(Double.self, forKey: .price)
        name = try container.decode(String.self, forKey: .name)
        if let value = try? container.decode(Int.self, forKey: .id) {
            id = String(value)
        } else {
            id = try container.decode(String.self, forKey: .id)
        }
    }
}

let json1 = """
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
"""

let json2 = """
{
"p":2.12,
"i":0,
"n":"Blue Shirt"
}
"""

do {
    let product = try JSONDecoder().decode(GeneralProduct.self, from: Data(json2.utf8))
    print(product.price ?? "")
    print(product.id ?? "")
    print(product.name ?? "")
} catch {
    print(error)
}

éditer/mettre à jour:

Vous pouvez également simplement affecter nil à votre id lorsque votre API retourne 0:

if let _ = try? container.decode(Int.self, forKey: .id) {
    id = nil
} else {
    id = try container.decode(String.self, forKey: .id)
}
29
Leo Dabus

C'est une solution possible avec MetadataType, ce qui est bien, c'est que cela peut être une solution générale non seulement pour GeneralProduct, mais pour tous les struct ayant la même ambiguïté:

struct GeneralProduct: Codable {
  var price:Double?
  var id:MetadataType?
  var name:String?

  private enum CodingKeys: String, CodingKey {
    case price = "p"
    case id = "i"
    case name = "n"
  }

  init(price:Double? = nil, id: MetadataType? = nil, name: String? = nil) {
    self.price = price
    self.id = id
    self.name = name
  }
}

enum MetadataType: Codable {
  case int(Int)
  case string(String)

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    do {
      self = try .int(container.decode(Int.self))
    } catch DecodingError.typeMismatch {
      do {
        self = try .string(container.decode(String.self))
      } catch DecodingError.typeMismatch {
        throw DecodingError.typeMismatch(MetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
      }
    }
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    switch self {
    case .int(let int):
      try container.encode(int)
    case .string(let string):
      try container.encode(string)
    }
  }
}

c'est le test:

let decoder = JSONDecoder()
var json =  "{\"p\":2.19,\"i\":0,\"n\":\"Black Shirt\"}"
var product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
  print(id) // 0
}

json =  "{\"p\":2.19,\"i\":\"hello world\",\"n\":\"Black Shirt\"}"
product = try! decoder.decode(GeneralProduct.self, from: json.data(using: .utf8)!)
if let id = product.id {
  print(id) // hello world
}
8
Andrea Mugnaini