使用混合类型和混合键控/非键控在 Swift 中解码 JSON

Posted

技术标签:

【中文标题】使用混合类型和混合键控/非键控在 Swift 中解码 JSON【英文标题】:Decoding JSON in Swift with Mixed Types and Mixed Keyed/Unkeyed 【发布时间】:2022-01-11 10:04:31 【问题描述】:

我正在努力解码 Swift 5 中的 JSON 结构,它看起来像下面的简化示例。我正在努力解决两个问题。外部数组是无键的,内部数组是有键的。最重要的是,内部数组偶尔包含混合类型 String 和 Int 的数组。我可以提供几十个根本不起作用的东西,但我只会提供 JSON:

[
  12,
  
    "a": [
      "orange",
      10,
      "purple"
    ],
    "b": [
      "red",
      9,
      "blue
    ],
    "c": [
      "yellow",
      "green"
    ]
  ,
  "string one",
  "string two"
]

感谢任何想法。

【问题讨论】:

异构 JSON 数组是一种非常糟糕的做法。您可以使用具有关联值的枚举和自定义初始化程序对其进行解码。但是,如果您能够更改 JSON,请执行此操作。 我希望我能改变它,但必须忍受它。是的,这是一种可怕的做法! 带有枚举和关联值的自定义init(from decoder: Decoder)方法是解决方案。 【参考方案1】:

可在 Playground 中使用的可能解决方案:

func heterogenousJSON() 
    let jsonStr = """
        [
        12,
        
        "a": [
        "orange",
        10,
        "purple"
        ],
        "b": [
        "red",
        9,
        "blue"
        ],
        "c": [
        "yellow",
        "green"
        ]
        ,
        "string one",
        "string two"
        ]
        """

    struct CodableStruct: Codable, CustomStringConvertible 
        let a: [CodableStructValues]
        let b: [CodableStructValues]
        let c: [String] //Here I set it as [String], but could be indeed [CodableStructValues], just to make it more "usual case"

        var description: String 
            " \"a\": [\(a.map $0.description .joined(separator: ", "))] \n" +
            " \"b\": [\(b.map $0.description .joined(separator: ", "))] \n" +
            " \"c\": [\(c.map $0 .joined(separator: ", "))] "
        
    

    enum CodableStructValues: Codable, CustomStringConvertible 
        case asInt(Int)
        case asString(String)

        init(from decoder: Decoder) throws 
            let values = try decoder.singleValueContainer()

            if let asInt = try? values.decode(Int.self) 
                self = .asInt(asInt)
                return
            
            //For the next: we didn't use `try?` but try, and it will throw if it's not a String
            // We assume that if it wasn't okay from previous try?, it's the "last chance". It needs to be of this type, or it will throw an error
            let asString = try values.decode(String.self)
            self = .asString(asString)
        

        var description: String 
            switch self 
            case .asInt(let intValue):
                return "\(intValue)"
            case .asString(let stringValue):
                return stringValue
            
        
    

    enum Heterogenous: Codable 
        case asInt(Int)
        case asString(String)
        case asCodableStruct(CodableStruct)

        init(from decoder: Decoder) throws 
            let values = try decoder.singleValueContainer()
            if let asInt = try? values.decode(Int.self) 
                self = .asInt(asInt)
                return
             else if let asString = try? values.decode(String.self) 
                self = .asString(asString)
                return
            
            //For the next: we didn't use `try?` but try, and it will throw if it's not a String
            // We assume that if it wasn't okay from previous try?, it's the "last chance". It needs to be of this type, or it will throw an error
            let asStruct = try values.decode(CodableStruct.self)
            self = .asCodableStruct(asStruct)
        
    

    do 
        let json = Data(jsonStr.utf8)
        let parsed = try JSONDecoder().decode([Heterogenous].self, from: json)
        print(parsed)

        parsed.forEach  aHeterogenousParsedValue in
            switch aHeterogenousParsedValue 
            case .asInt(let intValue):
                print("Got Int: \(intValue)")
            case .asString(let stringValue):
                print("Got String: \(stringValue)")
            case .asCodableStruct(let codableStruct):
                print("Got Struct: \(codableStruct)")
            
        
     catch 
        print("Error while decoding JSON: \(error)")
    


heterogenousJSON()

主要思想是使用 Codable enum with associated values 来保存所有异构值。然后你需要有一个自定义的init(from decoder: Decoder)。我创建了Codable 的值,但实际上我只创建了Decodable 部分。没有反向覆盖。

我使用CustomStringConvertible(及其description)来获得更易读的打印。

我在打印parsed 时添加了forEach(),以向您展示如何处理这些值。如果只需要一种情况,可以使用if case let 代替开关。

正如@vadian in comments 所说,在数组中具有这样的异构值并不是一个好习惯。你说在你的情况下你不能用后端开发改变它们,但我在这个答案中指出它,以防其他人有同样的问题,如果他/她可以改变它,推荐。

【讨论】:

非常感谢,这是一个非常好的解决方案。令人沮丧的是,这种东西来自 websocket 连接。我们这些接收端的人并没有太多考虑。

以上是关于使用混合类型和混合键控/非键控在 Swift 中解码 JSON的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL第十节:彩色键控与混合

将数组内容(自定义类型)写入 plist 作为 swift 中的键控数组

PHP如何处理整数索引的非连续键控数组?

在 Flink 中用 (X) 键控的缓慢变化流来丰富 (X,Y) 键控的快速流

Java加密技术—— HMACSHA1 加密算法

Textarea 不能与 jQuery 实时键控功能一起使用