swift 使用codable,其值有时是Int,有时是String

qco9c6ql  于 2023-10-15  发布在  Swift
关注(0)|答案(6)|浏览(129)

我有一个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
    }
}

我一直收到这个错误消息:Expected to decode String but found a number instead。它返回一个数字的原因是因为id字段为空,当id字段为空时,它默认返回0作为codable标识为数字的ID。我基本上可以忽略ID键,但codable并没有给予我忽略它的选项。处理这件事的最好办法是什么?
这里是JSON。超级简单
工作

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

错误-因为系统中没有id,它返回0作为默认值,codable显然将其视为数字而不是字符串。

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

n3h0vuf21#

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

3yhwsihp2#

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

pnwntuvh3#

IntString无错误地解码为相同的属性需要编写一些代码。
然而,由于语言中的一个(有点)新的添加,(属性 Package 器),你可以很容易地在任何你需要的地方重用这个逻辑:

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

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

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

原始答案

你可以在一个字符串上使用一个 Package 器,它知道如何从任何基本的JSON数据类型解码: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)
    }
}

然后,您可以在结构中使用此新类型。一个小缺点是,结构的使用者需要进行另一个间接访问来访问 Package 的字符串。但是,可以通过将解码后的RelaxedString属性声明为private来避免这种情况,并为public接口使用计算的属性:

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.不需要编写自定义的init(from decoder: Decoder)代码,如果要解码的属性数量增加,
1.可重用性-RelaxedString可以无缝地用于其他结构

  1. id可以从字符串或int解码的事实仍然是一个实现细节,GeneralProduct的消费者不知道/关心id可以来自字符串或int。
    1.公共接口公开字符串值,这使消费者代码保持简单,因为它不必处理多种类型的数据
flseospp

flseospp4#

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

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

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

cgyqldqp

cgyqldqp5#

根据@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? 
}

我想-

struct AnotherDTO: Codable {
    var some: SomeDTO?
}
wz3gfoph

wz3gfoph6#

你可以用这个pod https://github.com/muhammadali2012/Model
简单地将这些属性 Package 器添加到类型不确定的可编码属性上。ie

@AnyValueWrapper @DefaultStringEmpty var id: String

你会得到id作为字符串,即使你从JSON中得到int,甚至nill,甚至key不存在。

相关问题