Using codable with key that is sometimes an Int and other times a String

荒凉一梦 提交于 2019-11-26 05:31:41

问题


I have an API that will sometimes return a specific key (in this case id) in the JSON as an Int and other times it will return that same key as a String. How do I use codable to parse that 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
    }
}

I keep getting this error message: Expected to decode String but found a number instead. The reason that it returns a number is because the id field is empty and when the id field is empty it defaults to returning 0 as an ID which codable identifies as a number. I can basically ignore the ID key but codable does not give me the option to ignore it to my knowledge. What would be the best way to handle this?

Here is the JSON. It is super simple

Working

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

Error - because there is no id in the system, it returns 0 as a default which codable obviously sees as a number opposed to string.

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

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

edit/update:

You can also simply assign nil to your id when your api returns 0:

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



回答2:


This is a possible solution with MetadataType, the nice thing is that can be a general solution not for GeneralProduct only, but for all the struct having the same ambiguity:

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

this is the 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
}


来源:https://stackoverflow.com/questions/47935705/using-codable-with-key-that-is-sometimes-an-int-and-other-times-a-string

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!