使用带有密钥的可编码,有时是Int,有时是String

Nev*_*ani 26 json swift swift4 codable

我有一个API,有时会id在JSON中作为Int 返回一个特定的键(在本例中),有时它将返回与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
    }
}
Run Code Online (Sandbox Code Playgroud)

我不断收到此错误消息:Expected to decode String but found a number instead.返回数字的原因是因为id字段为空,并且当id字段为空时,它默认返回0作为可编码标识为数字的ID.我基本上可以忽略ID键但是可编码不会让我根据我的知识忽略它.处理这个问题的最佳方法是什么?

这是JSON.它非常简单

工作

{
  "p":2.12,
  "i":"3k3mkfnk3",
  "n":"Blue Shirt"
}
Run Code Online (Sandbox Code Playgroud)

错误 - 因为系统中没有id,它返回0作为默认值,可编码显然看作是与字符串相对的数字.

{
  "p":2.19,
  "i":0,
  "n":"Black Shirt"
}
Run Code Online (Sandbox Code Playgroud)

Leo*_*bus 36

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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
let json1 = """
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
"""

let json2 = """
{
"p":2.12,
"i":0,
"n":"Blue Shirt"
}
"""
Run Code Online (Sandbox Code Playgroud)
do {
    let product = try JSONDecoder().decode(GeneralProduct.self, from: Data(json2.utf8))
    print(product.price ?? "")
    print(product.id ?? "")
    print(product.name ?? "")
} catch {
    print(error)
}
Run Code Online (Sandbox Code Playgroud)

编辑/更新:

您也可以简单地分配nil给您id的api何时返回0:

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


Cri*_*tik 15

您可以在字符串上使用包装器,该字符串知道如何从任何基本 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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
    }
}
Run Code Online (Sandbox Code Playgroud)

上述方法的优点:

  1. 无需编写自定义init(from decoder: Decoder)代码,如果要解码的属性数量增加,这会变得乏味
  2. 可重用性 -RelaxedString可以在其他结构中无缝使用
  3. 可以从字符串或 int 中解码 id 的事实仍然是一个实现细节,消费者GeneralProduct不知道/不关心 id 可以来自字符串或 int
  4. 公共接口公开字符串值,这使消费者代码保持简单,因为它不必处理多种类型的数据


And*_*ini 12

这是一个可能的解决方案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)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这是测试:

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
}
Run Code Online (Sandbox Code Playgroud)

  • 这不是打印出“int(0)”而不是“0”吗?正如在“if let id = Product.id”行中分配的类型仍然是“MetadataType”类型 (5认同)
  • 很棒的答案!如果您想直接访问相关值,[这里有一些帮助](/sf/ask/1698447761/). (2认同)

Amr*_*gry 7

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

case stringValue(String)
case intValue(Int)
case doubleValue(Double)
case boolValue(Bool)
Run Code Online (Sandbox Code Playgroud)

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