【问题标题】:Is it possible to decode non-homogenous arrays using Swift/Codable?是否可以使用 Swift/Codable 解码非同质数组?
【发布时间】:2021-01-14 23:17:40
【问题描述】:

我需要解码数组中的 JSON 数据,其中第一个元素是与以下所有对象不同的对象。我想丢弃第一个数组元素并解码其余元素。 Swift Codable 可以做到这一点吗?

这是我在 Swift Playground 中整理的一个示例,用于说明正在发生的事情

import Foundation
class Test: Codable {
    var name:String = ""
    var number:Int = -1
}
var json1 = """
[[{"Type":"Testing", "Name":23}],
[{"name":"Rec1", "number":2}],
[{"name":"Rec2", "number":3}]]
"""
var json2 = """
[[{"name":"Rec12", "number":22}],
[{"name":"Rec22", "number":32}]]
"""
do{
    let test1 = try JSONDecoder().decode([[Test]].self, from: json1.data(using: .utf8)!)
    for t in test1 {
        print("\(t[0].name) \(t[0].number)")
    }
}catch{
    print("Exception thrown 1: \(error)")
}
do{
    let test2 = try JSONDecoder().decode([[Test]].self, from: json2.data(using: .utf8)!)
    for t in test2 {
        print("\(t[0].name) \(t[0].number)")
    }
}catch{
    print("Exception thrown 2: \(error)")
}

输出不足为奇。解码json1会抛出异常,解码json2不会:

Exception thrown 1: keyNotFound(CodingKeys(stringValue: "name", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"name\", intValue: nil) (\"name\").", underlyingError: nil))
Rec12 22
Rec22 32

我无法控制这些数据,我不能要求供应商做得更好——如果只是的话。

我能看到的唯一方法是在将 JSON 数据提供给 JSONDecoder 之前对其进行文本处理。我是 Swift 新手,我希望有比这更好的方法......

private func _hackJSONArray(_ input:Data) throws -> Data {
    /*
     Input data is of the form:
     [
       [{"Type":"Testing", "Name":23}],
       [{"name":"Rec1", "number":2}],
       [{"name":"Rec2", "number":3}]
     ]
     
     That is the first element is not like the others
     */
    var instr = String(data:input, encoding:.utf8)!
    if instr.prefix(2) != "[[" {
        throw ErrorItem(theErrorType: .InvalidJSON(instr), theDescription: "Failed to decode JSON data.  Did not start with: \"[[\"")
    }
    if let endOfFirstElement = instr.firstIndex(of: "]") {
        let first = instr.index(instr.startIndex, offsetBy:1)
        let last = instr.index(endOfFirstElement, offsetBy: 2)
        instr.removeSubrange(first..<last)
    }else{
        throw ErrorItem(theErrorType: .InvalidJSON(instr), theDescription: "Failed to decode JSON data.  Could not find: \"]\"")
    }
    return instr.data(using: .utf8)!
}

【问题讨论】:

  • 只有一个元素的数组?
  • 是的,在此处的示例中。我正在处理的实际数据在很多方面都是不正当的。这只是其中一个问题的玩具示例
  • 如果问题没有如实提出,很难提供帮助......

标签: json swift codable


【解决方案1】:

要处理可变数据结构,您必须为测试类型编写自己的解码器以及定义可能字段的 CodingKeys 枚举。方法是尝试解码其中一个可能的字段,如果失败捕获错误并在 catch 块中尝试解码另一种可能性:

class Test: Decodable {
   let name: String
   let number: Int
   
   enum CodingKeys:  String, CodingKey{
      case other = "Type".   //required as Type is a reserved keyword, so can't be an enum case
      case Name
      case name
      case number
      
   }
      
   required public init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: CodingKeys.self)
      do {
         name = try container.decode(String.self, forKey: .other)
      } catch {
         name = try container.decode(String.self, forKey: .name)
      }
      
      do {
         number = try container.decode(Int.self, forKey: .Name)
      } catch {
         number = try container.decode(Int.self, forKey: .number)
      }
   }
}

【讨论】:

  • 您的意思是,如果我理解的话,是为了处理非同构数组,将其视为其中所有对象联合的同构数组,其中每个字段实际上都是可选的。
  • @Worik 不完全。我将键视为变量而不是数据本身;在此示例中,所有数组对象仍然是 OP 的测试类型,只是它们在 JSON 中以不同的方式键入。如果数据本身是非同质的,则需要一种不同的方法,为此我可能会使用带有枚举和关联值的数据结构(您可以使用多种可选数据类型,但长期来看会很混乱)
【解决方案2】:

所以,flanker 的回答很优雅。但问题是它用不正确的数据填充了第一个数组的元素。作者说应该是扔掉的可能性大于被映射的可能性。所以在我看来,不是一个优雅但直接的解决方案是将您的数据结构字段声明为Optional

struct Test: Codable {
    var name: String?
    var number: Int?
}

然后检查你的结果是否有 nil 值:

testArray.filter({obj in
    return obj.name == nil ? false : true
})

【讨论】:

    【解决方案3】:

    既然你只想扔掉无效元素,而且 Swift 支持对可选项的解码,所以只需制作可以将任何 Element 数组转换为 Element? 数组的通用包装器,解码包装类型的数组,然后使用 compactMap 丢弃任何 nil 值。

    例如:

    let jsonData =
    """
    [
    {"name": "a"},
    {"name": "b"},
    {"malformed": 1 },
    {"name": "c"},
    ]
    """.data(using: .utf8)!
    
    struct ThingToDecode: Decodable {
      var name: String
    }
    
    struct OptionalElement<WrappedDecodable: Decodable> : Decodable {
      var value: WrappedDecodable?
      init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        value = try? container.decode(WrappedDecodable.self)
      }
    }
    
    let decoded = (try! JSONDecoder().decode([OptionalElement<ThingToDecode>].self, from: jsonData)).compactMap(\.value)
    
    print(decoded)
    

    请注意,如果您确实想要异构类型,那么您可以使用枚举来保留所有元素。在这种情况下,您可以自己编写它,但将其粘贴到 QuickType.io 中会更快,它实际上会为您生成枚举。

    【讨论】:

      【解决方案4】:

      在模型中将属性设为可选。并过滤[Test]数组的非零元素。这种方法仍然会解码所有属性,包括未格式化的NameType,并在过滤数组后将其删除。

      struct Test: Codable {
          var name: String?
          var number: Int?
      }
      
      var json1 = """
      [
      [{"Type":"Testing", "Name":23}],
      [{"name":"Rec1", "number":2}],
      [{"Type":"Testing", "Name":23}],
      [{"name":"Rec2", "number":3}],
      [{"Type":"Testing", "Name":23}]
      ]
      """
      
          do{
              let test1 = try JSONDecoder().decode([[Test]].self, from: json1.data(using: .utf8)!)
              let result = test1.filter { $0.first?.name != nil }
              print(result)
          } catch {
              print("Exception thrown 1: \(error)")
          }
      

      控制台:

      [[TestProject5.Test(name: Optional("Rec1"), number: Optional(2))], [TestProject5.Test(name: Optional("Rec2"), number: Optional(3))]]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-06-27
        • 1970-01-01
        • 1970-01-01
        • 2018-01-09
        • 1970-01-01
        • 1970-01-01
        • 2019-05-04
        相关资源
        最近更新 更多