如何正确解析带有根元素的 JSON 作为 Swift 中的数组?

Posted

技术标签:

【中文标题】如何正确解析带有根元素的 JSON 作为 Swift 中的数组?【英文标题】:How to correctly parse JSON with root element as an array in Swift? 【发布时间】:2020-07-09 10:50:32 【问题描述】:

我有以下问题,我没有正确的结构来解码来自 node-red 的响应以了解我的 Sonos 播放器的状态:

[["urlHostname":"192.168.1.1","urlPort":1400,"baseUrl":"http://192.168.1.1:1400","sonosName":"Sonos1","uuid":"RINCON_SONOS14001400","invisible":false,"urlHostname":"192.168.1.2","urlPort":"1400","baseUrl":"http://192.168.1.2:1400","sonosName":"Sonos2","uuid":"RINCON_SONOS21400","invisible":false,"urlHostname":"192.168.1.3","urlPort":"1400","baseUrl":"http://192.168.1.3:1400","sonosName":"Sonos3","uuid":"RINCON_SONOS31400","invisible":false,"urlHostname":"192.168.1.4","urlPort":"1400","baseUrl":"http://192.168.1.4:1400","sonosName":"Sonos4","uuid":"RINCON_SONOS41400","invisible":false,"urlHostname":"192.168.1.5","urlPort":"1400","baseUrl":"http://192.168.1.5:1400","sonosName":"Sonos5","uuid":"RINCON_SONOS51400","invisible":false,"urlHostname":"192.168.1.6","urlPort":"1400","baseUrl":"http://192.168.1.6:1400","sonosName":"Sonos6","uuid":"RINCON_SONOS61400","invisible":false]]

我的结构和解码调用如下所示:

typealias Response = [sonosData]

struct sonosData: Codable 
    let  sonos: [sonosStatusEntry]?


struct sonosStatusEntry: Codable 
    let status: [sonosStatus]?


struct sonosStatus: Codable 
    let urlHostname: String
    let urlPort: Int
    let baseUrl: String
    let sonosName: String
    let uuid: String
    let invisible: Bool


let response = try JSONDecoder().decode(Response.self, from: data)

我在 Swift 中收到以下错误:

Failed to load: The data couldn’t be read because it isn’t in the correct format.

有什么建议吗?

【问题讨论】:

打印错误,而不是 error.localizedDescription。它将有更多信息。您会看到没有sonos 键。应该只是let sonos = try JSONDecoder().decode([sonosStatus].self, from: data) JSON 中的 sonosstatus 键在哪里?并且名称结构总是以大写字母开头。 你从哪里得到错误?是JSONDecoder().decode抛出的吗? 谢谢,有帮助。改为 let sonos = try JSONDecoder().decode([sonosStatus].self, from: data) 【参考方案1】:

您的代码有 2 个错误:

    您要解码的数据是[[sonosStatus]]。所以你需要解码为:
let response = try JSONDecoder().decode([[sonosStatus]].self, from: data)
    urlPort 代表 StringInt。因此,您需要更正 JSON 格式或使用自定义解码器支持这两种格式。

您可以使用这个简单的属性包装器来实现:

@propertyWrapper
struct SomeKindOfInt: Codable 
    var wrappedValue: Int?

    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        if let stringifiedValue = try? container.decode(String.self) 
            wrappedValue = Int(stringifiedValue)
         else 
            wrappedValue = try container.decode(Int.self)
        
    

然后像这样使用它:

struct sonosStatus: Codable 
    let urlHostname: String
    @SomeKindOfInt var urlPort: Int?
    let baseUrl: String
    let sonosName: String
    let uuid: String
    let invisible: Bool

【讨论】:

将两者都添加到我的代码中,现在可以正常工作了!谢谢你的帮助!!! 欢迎您。如果有帮助别忘了点赞。如果这是您问题的答案,也将其标记为已接受的答案。【参考方案2】:

其中一个问题与urlPort 有关。您的 json 具有 IntStringurlPort 值。您可以定义自定义初始化来处理它。

struct sonosStatus: Codable 
    let urlHostname: String
    let urlPort: Int
    let baseUrl: String
    let sonosName: String
    let uuid: String
    let invisible: Bool    
    
    init(from decoder: Decoder) throws 
        let values = try decoder.container(keyedBy: CodingKeys.self)
        urlHostname = try values.decode(String.self, forKey: .urlHostname)
        baseUrl = try values.decode(String.self, forKey: .baseUrl)
        sonosName = try values.decode(String.self, forKey: .sonosName)
        uuid = try values.decode(String.self, forKey: .uuid)
        invisible = try values.decode(Bool.self, forKey: .invisible)
        
        // you can define urlPort as optional or unwrap it.
        if let intVal = try? values.decode(Int.self, forKey: .urlPort) 
            urlPort = intVal
         else if let stringVal = try? values.decode(String.self, forKey: .urlPort) 
            urlPort = Int(stringVal) ?? 0
         else 
            urlPort = ""
        
    

而且你的 json 是一个 sonosStatus 数组。

所以你需要修改Response typealias,如下所示:

typealias Response = [[sonosStatus]]

【讨论】:

【参考方案3】:

您可以将 JSONSerialization 与 .allowFragments 读取选项一起使用。

let objects = (try?JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [[NSDictionary]]

它已在最新的 Swift 版本上得到修复:https://bugs.swift.org/browse/SR-6163

【讨论】:

答案与问题无关。如果预期的类型是数组或字典,allowFragments 是没有意义的。并且永远不要在 Swift 中将 JSON 数据转换为 NS... 集合类型 @vadian 我认为根本原因是 json 的顶层是一个数组,而不是字典。 JSONSerializationallowFragments 解决它。对于 NSDictionary 的强制转换,为什么我们做不到呢? 不,allowFragments 用于原始***类型,例如 StringInt。就 JSON 而言,数组和字典都是对象。并且转换为 NSDictionary 会丢失(强)类型信息,请始终使用本机 Seift 类型。 yaaaa,我明白了;)

以上是关于如何正确解析带有根元素的 JSON 作为 Swift 中的数组?的主要内容,如果未能解决你的问题,请参考以下文章

正确解析 Json 输出

如何使用 javascript 解析带有数组的外部 JSON 文件

避免将 null 作为视图根传递(需要在膨胀布局的根元素上解析布局参数)

Android Kotlin + Klaxon - 解析 JSON 根数组

如何使用 JSON 路径引用根键?

二十八xml编程(domxpathsimplexml)