使用可编码的值,有时是 Int,有时是 String

发布于 2025-01-17 13:17:11 字数 911 浏览 3 评论 0 原文

我有一个 API,有时会以 Int 形式返回 JSON 中的特定键值(在本例中为 id),有时会以 String 形式返回相同的键值。如何使用 codable 来解析该 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
    }
}

我不断收到此错误消息:预期解码字符串,但发现了一个数字。它返回数字的原因是因为 id 字段为空,当 id 字段为空时,它默认返回 0 作为 ID,可编码识别为数字。我基本上可以忽略 ID 密钥,但据我所知,可编码并没有给我忽略它的选项。处理这个问题的最佳方法是什么?

这是 JSON。这是超级简单的

工作

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

错误 - 因为系统中没有 id,所以它默认返回 0,可编码显然将其视为与字符串相反的数字。

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

I have an API that will sometimes return a specific key value (in this case id) in the JSON as an Int and other times it will return that same key value 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"
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

伪心 2025-01-24 13:17:11
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)
        do {
            id = try String(container.decode(Int.self, forKey: .id))
        } catch DecodingError.typeMismatch {
            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 ?? "nil")
    print(product.id ?? "nil")
    print(product.name ?? "nil")
} catch {
    print(error)
}

编辑/更新

当您的 api 返回 0 时,您也可以简单地将 nil 分配给您的 id

do {
    let value = try container.decode(Int.self, forKey: .id)
    id = value == 0 ? nil : String(value)
} catch DecodingError.typeMismatch {
    id = try container.decode(String.self, forKey: .id)
}
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)
        do {
            id = try String(container.decode(Int.self, forKey: .id))
        } catch DecodingError.typeMismatch {
            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 ?? "nil")
    print(product.id ?? "nil")
    print(product.name ?? "nil")
} catch {
    print(error)
}

edit/update:

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

do {
    let value = try container.decode(Int.self, forKey: .id)
    id = value == 0 ? nil : String(value)
} catch DecodingError.typeMismatch {
    id = try container.decode(String.self, forKey: .id)
}
一萌ing 2025-01-24 13:17:11

这是 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
}

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
}
握住我的手 2025-01-24 13:17:11

int 字符串中无缝解码为同一属性需要编写一些代码。

但是,由于(属性包装纸)(属性包装器),您可以在任何需要的地方重复使用此逻辑非常容易:

// note this is only `Decodable`
struct GeneralProduct: Decodable {
    var price: Double
    @Flexible var id: Int // note this is an Int
    var name: String
}

属性包装器及其支持代码可以像这样实现:

@propertyWrapper struct Flexible<T: FlexibleDecodable>: Decodable {
    var wrappedValue: T
    
    init(from decoder: Decoder) throws {
        wrappedValue = try T(container: decoder.singleValueContainer())
    }
}

protocol FlexibleDecodable {
    init(container: SingleValueDecodingContainer) throws
}

extension Int: FlexibleDecodable {
    init(container: SingleValueDecodingContainer) throws {
        if let int = try? container.decode(Int.self) {
            self = int
        } else if let string = try? container.decode(String.self), let int = Int(string) {
            self = int
        } else {
            throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Invalid int value"))
        }
    }
}

原始答案

您可以通过知道如何从任何基本JSON数据类型解码的字符串上使用包装器:字符串,数字,布尔值:

struct RelaxedString: Codable {
    let value: String
    
    init(_ value: String) {
        self.value = value
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        // attempt to decode from all JSON primitives
        if let str = try? container.decode(String.self) {
            value = str
        } else if let int = try? container.decode(Int.self) {
            value = int.description
        } else if let double = try? container.decode(Double.self) {
            value = double.description
        } else if let bool = try? container.decode(Bool.self) {
            value = bool.description
        } else {
            throw DecodingError.typeMismatch(String.self, .init(codingPath: decoder.codingPath, debugDescription: ""))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value)
    }
}

然后,您可以在结构中使用此新类型。一个较小的缺点是结构的消费者需要进行另一个间接的访问以访问包裹的字符串。但是,可以通过将解码的放松属性为私有来避免这种避免,并在公共接口中使用计算的属性:

struct GeneralProduct: Codable {
    var price: Double!
    var _id: RelaxedString?
    var name: String!
    
    var id: String? {
        get { _id?.value }
        set { _id = newValue.map(RelaxedString.init) }
    }

    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.map(RelaxedString.init)
        self.name = name
    }
}

上述方法的优点:

  1. 无需编写Custom Init(来自解码器::解码器)代码,如果要解码的属性数量增加可重复
  2. 使用性 - 可以在其他结构中无缝地使用
  3. ID可以从字符串或字符串或INT仍然是实现细节, GeneralProduct 的消费者不知道/关心ID可以来自字符串或int
  4. 公共接口曝光字符串值,这使消费者代码保持简单不必处理多种类型的数据

Seamlessly decoding from either Int or String into the same property requires writing some code.

However, thanks to a (somewhat) new addition to the language,(property wrappers), you can make it quite easy to reuse this logic wherever you need it:

// note this is only `Decodable`
struct GeneralProduct: Decodable {
    var price: Double
    @Flexible var id: Int // note this is an Int
    var name: String
}

The property wrapper and its supporting code can be implemented like this:

@propertyWrapper struct Flexible<T: FlexibleDecodable>: Decodable {
    var wrappedValue: T
    
    init(from decoder: Decoder) throws {
        wrappedValue = try T(container: decoder.singleValueContainer())
    }
}

protocol FlexibleDecodable {
    init(container: SingleValueDecodingContainer) throws
}

extension Int: FlexibleDecodable {
    init(container: SingleValueDecodingContainer) throws {
        if let int = try? container.decode(Int.self) {
            self = int
        } else if let string = try? container.decode(String.self), let int = Int(string) {
            self = int
        } else {
            throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Invalid int value"))
        }
    }
}

Original answer

You can use a wrapper over a string that knows how to decode from any of the basic JSON data types: string, number, boolean:

struct RelaxedString: Codable {
    let value: String
    
    init(_ value: String) {
        self.value = value
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        // attempt to decode from all JSON primitives
        if let str = try? container.decode(String.self) {
            value = str
        } else if let int = try? container.decode(Int.self) {
            value = int.description
        } else if let double = try? container.decode(Double.self) {
            value = double.description
        } else if let bool = try? container.decode(Bool.self) {
            value = bool.description
        } else {
            throw DecodingError.typeMismatch(String.self, .init(codingPath: decoder.codingPath, debugDescription: ""))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(value)
    }
}

You can then use this new type in your struct. One minor disadvantage would be that consumer of the struct will need to make another indirection to access the wrapped string. However that can be avoided by declaring the decoded RelaxedString property as private, and use a computed one for the public interface:

struct GeneralProduct: Codable {
    var price: Double!
    var _id: RelaxedString?
    var name: String!
    
    var id: String? {
        get { _id?.value }
        set { _id = newValue.map(RelaxedString.init) }
    }

    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.map(RelaxedString.init)
        self.name = name
    }
}

Advantages of the above approach:

  1. no need to write custom init(from decoder: Decoder) code, which can become tedious if the number of properties to be decoded increase
  2. reusability - RelaxedString can be seamlessly used in other structs
  3. the fact that the id can be decoded from a string or an int remains an implementation detail, consumers of GeneralProduct don't know/care that the id can come from a string or an int
  4. the public interface exposes string values, which keeps the consumer code simple as it will not have to deal with multiple types of data
风尘浪孓 2025-01-24 13:17:11

我创建了这个 Gist,它有一个 ValueWrapper 结构可以处理
以下类型

case stringValue(String)
case intValue(Int)
case doubleValue(Double)
case boolValue(Bool)

https://gist.github.com/amrangry/89097b86514b3477cae79dd28bba3f23

I created this Gist which has a ValueWrapper struct that can handle
the following types

case stringValue(String)
case intValue(Int)
case doubleValue(Double)
case boolValue(Bool)

https://gist.github.com/amrangry/89097b86514b3477cae79dd28bba3f23

魔法少女 2025-01-24 13:17:11

根据@cristik的答案,我使用 @propertywrapper 提供了另一种解决方案。

@propertyWrapper
struct StringForcible: Codable {
    
    var wrappedValue: String?
    
    enum CodingKeys: CodingKey {}
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self) {
            wrappedValue = string
        } else if let integer = try? container.decode(Int.self) {
            wrappedValue = "\(integer)"
        } else if let double = try? container.decode(Double.self) {
            wrappedValue = "\(double)"
        } else if container.decodeNil() {
            wrappedValue = nil
        }
        else {
            throw DecodingError.typeMismatch(String.self, .init(codingPath: container.codingPath, debugDescription: "Could not decode incoming value to String. It is not a type of String, Int or Double."))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
    
    init() {
        self.wrappedValue = nil
    }
    
}

用法也

struct SomeDTO: Codable {
   @StringForcible var id: String? 
}

像-i认为 -

struct AnotherDTO: Codable {
    var some: SomeDTO?
}

Based on @Cristik 's answer, I come with another solution using @propertyWrapper.

@propertyWrapper
struct StringForcible: Codable {
    
    var wrappedValue: String?
    
    enum CodingKeys: CodingKey {}
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self) {
            wrappedValue = string
        } else if let integer = try? container.decode(Int.self) {
            wrappedValue = "\(integer)"
        } else if let double = try? container.decode(Double.self) {
            wrappedValue = "\(double)"
        } else if container.decodeNil() {
            wrappedValue = nil
        }
        else {
            throw DecodingError.typeMismatch(String.self, .init(codingPath: container.codingPath, debugDescription: "Could not decode incoming value to String. It is not a type of String, Int or Double."))
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
    
    init() {
        self.wrappedValue = nil
    }
    
}

And usage is

struct SomeDTO: Codable {
   @StringForcible var id: String? 
}

Also works like -I think-

struct AnotherDTO: Codable {
    var some: SomeDTO?
}
够钟 2025-01-24 13:17:11

您可以使用此 pod https://github.com/muhammadali2012/Model

只需将这些属性包装器添加到您的可编码属性的类型不确定。即,

@AnyValueWrapper @DefaultStringEmpty var id: String

即使您从 JSON 中获取 int 或什至 nill 或即使 key 不存在,您也会获得 String 形式的 id。

you can use this pod https://github.com/muhammadali2012/Model

Simply add these property wrappers on your codable properties which type is not sure. ie

@AnyValueWrapper @DefaultStringEmpty var id: String

you will get id as String even if you get int from JSON or even nill or even if key doesn't exist.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文