Подтвердить что ты не робот

Использование codable с ключом, который иногда является Int и другими временами String

У меня есть API, который иногда возвращает определенный ключ (в этом случае id) в JSON как Int, а в других случаях он возвращает тот же самый ключ, что и String. Как использовать кодируемый для анализа 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
    }
}

Я продолжаю получать это сообщение об ошибке: Expected to decode String but found a number instead. Причина, по которой он возвращает число, состоит в том, что поле id пустое, а когда поле id пусто, оно по умолчанию возвращает 0 в качестве идентификатора, который кодируемый идентифицирует как число. Я могу в принципе игнорировать ID-ключ, но способный код не дает мне возможность игнорировать его, насколько мне известно. Какой был бы лучший способ справиться с этим?

Вот JSON. Это супер просто

За работой

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

Ошибка - потому что в системе нет идентификатора, она возвращает 0 как значение по умолчанию, которое, по-видимому, воспринимается как число, противоположное строке.

{
  "p":2.19,
  "i":0,
  "n":"Black Shirt"
}
4b9b3361

Ответ 1

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

изменить/обновить:

Вы также можете просто назначить nil вашему id когда ваш api вернет 0:

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

Ответ 2

Это возможное решение с MetadataType, хорошо, что это может быть общее решение не только для GeneralProduct, но для всей struct имеющей ту же двусмысленность:

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

это тест:

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
}