如何在 Swift 中使用动态键(在根级别)解码此 JSON?

Posted

技术标签:

【中文标题】如何在 Swift 中使用动态键(在根级别)解码此 JSON?【英文标题】:How to decode this JSON with dynamic keys (on the root level) in Swift? 【发布时间】:2021-09-15 19:30:51 【问题描述】:

我正在尝试创建一个小应用程序来使用 SwiftUI 控制我的 Hue 灯,但我无法通过这个 JSON 进行解码/迭代。关于如何做到这一点,有 这么多 线程,我已经尝试了数十个,使用 CodingKeys,创建自定义解码器等等,但我似乎无法理解。所以这是我从 Hue Bridge 获得的 JSON:


    "1": 
        "state": 
            "on": true,
            "bri": 254,
            "hue": 8417,
            "sat": 140,
            "effect": "none",
            "xy": [
                0.4573,
                0.41
            ],
            "ct": 366,
            "alert": "select",
            "colormode": "ct",
            "mode": "homeautomation",
            "reachable": false
        ,
        "swupdate": 
            "state": "noupdates",
            "lastinstall": "2021-08-26T12:56:12"
        ,
        ...
    ,
    "2": 
        "state": 
            "on": false,
            "bri": 137,
            "hue": 36334,
            "sat": 203,
            "effect": "none",
            "xy": [
                0.2055,
                0.3748
            ],
            "ct": 500,
            "alert": "select",
            "colormode": "xy",
            "mode": "homeautomation",
            "reachable": true
        ,
        "swupdate": 
            "state": "noupdates",
            "lastinstall": "2021-08-13T12:29:48"
        ,
        ...
    ,
    "9": 
        "state": 
            "on": false,
            "bri": 254,
            "hue": 16459,
            "sat": 216,
            "effect": "none",
            "xy": [
                0.4907,
                0.4673
            ],
            "alert": "none",
            "colormode": "xy",
            "mode": "homeautomation",
            "reachable": true
        ,
        "swupdate": 
            "state": "noupdates",
            "lastinstall": "2021-08-12T12:51:18"
        ,
        ...
    ,
    ...

所以结构基本上是根级别的动态键和光信息,这是相同的。我创建了以下类型:

struct LightsObject: Decodable 
    public let lights: [String:LightInfo]

    
struct LightInfo: Decodable 
    var state: StateInfo
    struct StateInfo: Decodable 
        var on: Bool
        var bri: Int
        var hue: Int
        var sat: Int
        var effect: String
        var xy: [Double]
        var ct: Int
        var alert: String
        var colormode: String
        var reachable: Bool
    
    
    var swupdate: UpdateInfo
    struct UpdateInfo: Decodable 
        var state: String
        var lastinstall: Date
    
    ...

(它基本上继续包含来自对象的所有变量)

我现在遇到的问题是,我似乎无法将它放入普通数组或字典中。我会选择像 "1":LightInfo, "2", LightInfo 这样我可以迭代的东西,或者只是一个简单的 [LightInfo, LightInfo, ...],因为我什至可能不需要索引。

然后,理想情况下,我可以做类似的事情

ForEach(lights)  light in
   Text(light.name)

我尝试过创建自定义编码键,将类型实现为 Codable,等等,但我找不到适合我的解决方案。我知道,关于这个主题有很多线程,但我觉得我的初始设置可能是错误的,这就是它不起作用的原因。

这是解码部分:

            let task = urlSession.dataTask(with: apiGetLightsUrl, completionHandler:  (data, response, error) in
                guard let data = data, error == nil else  return 
                
                do 
                    let lights = try JSONDecoder().decode([String: LightInfo].self, from: data)
                    completionHandler(lights, nil)
                 catch 
                    print("couldn't get lights")
                    completionHandler(nil, error)
                
            )

实际上得到了 JSON,没问题,但正如我所说,我还不能解码它。最新的错误是:

Optional(Swift.DecodingError.typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "1", intValue: nil), CodingKeys(stringValue: "swupdate", intValue: nil), CodingKeys(stringValue: "lastinstall", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil)))

我看过一些帖子,但他们使用的 JSON 通常有一个***对象,比如


   "foo":
      
         "bar": "foobar"
         ...
      ,
      
         "bar": "foobar2"
         ...
      ,
      
         "bar": "foobar3"
         ...
      
   ...

所以处理有点不同,因为我可以创建一个类似的结构

struct Object: Decodable 
    var foo: [LightsInfo]

这是不可能的:(

谁能指出我正确的方向?谢谢!

更新:

谢谢大家,解决方案确实有效。我的结构中有一些错误,现在已修复。 (拼写错误的变量名、可选值等)。现在唯一缺少的是如何遍历字典。如果我使用

ForEach(lights)...

Swift 抱怨说它只应该用于静态字典。我试过这样做

ForEach(lights, id: \.key)  key, value in
   Button(action: print(value.name)) 
      Text("foo")
   

但我收到此错误:Generic struct 'ForEach' requires that '[String : LightInfo]' conform to 'RandomAccessCollection'

更新 2:

所以这似乎有效:

struct LightsView: View 
    @Binding var lights: [String:LightInfo]
    
    var body: some View 
        VStack 
            ForEach(Array(lights.keys.enumerated()), id: \.element)  key, value in
                LightView(lightInfo: self.lights["\(value)"]!, lightId: Int(value) ?? 0)
            
        
    

我将尝试清理代码并对其进行一些优化。欢迎提出建议;)

【问题讨论】:

app.quicktype.io 错误很明显。请仔细阅读错误信息。键 lastinstall 的值是 String,而不是 Date。您需要适当的日期解码策略将值直接解码为Date 如果解决方案确实有效,请投票选出最佳解决方案。 【参考方案1】:

你似乎快到了。这是[String:LightInfo]。您只需要对其进行解码(而不是将其包装在 JSON 中不存在的 LightsObject 中)。如果您不关心数字,则可以取消这些值。应该是这样的:

let lights = try JSONDecoder().decode([String: LightInfo].self, from: data).values

【讨论】:

嗨,罗伯!谢谢你的建议。我从来不知道,还有另一个级别,我可以在其中检索值......但是,编译器现在抱怨类型。这会是 Dictionary.Values 吗?好像有点奇怪。 是的,这将是Dictionary<String, LightObject>.Values。如果你想要一个数组,你可以用Array(lights) 转换它。 Values 类型是惰性的;它不会强制复制。当你将它包装成一个数组时,它必须实际复制东西(如果这是你想要的)。【参考方案2】:

我自己也遇到了类似的问题,我想处理给定的通用键。这就是我解决它的方法,适应你的代码。

struct LightsObject: Decodable 
    public var lights: [String:LightInfo]
    
    private enum CodingKeys: String, CodingKey 
        case lights
    
    
    // Decode the JSON manually
    required public init(from decoder: Decoder) throws 
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.lights = [String:LightInfo]()
        if let lightsSubContainer = try? container.nestedContainer(keyedBy: GenericCodingKeys.self, forKey: .lights) 
            for key in lightsSubContainer.allKeys 
                if let lightInfo = try? lightsSubContainer.decode(LightInfo.self, forKey: key) 
                    self.lights?[key.stringValue] = lightInfo
                
            
        
    


public class GenericCodingKeys: CodingKey 
    public var stringValue: String
    public var intValue: Int?
    
    required public init?(stringValue: String) 
        self.stringValue = stringValue
    
    
    required public init?(intValue: Int) 
        self.intValue = intValue
        stringValue = "\(intValue)"
    

【讨论】:

嗨,山姆!所以我尝试了你的解决方案。不得不将 LightsObject 更改为一个类,因为 Swift 抱怨 init 函数。当我现在尝试对其进行解码时,我仍然收到类似的错误:Optional(Swift.DecodingError.typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "1", intValue: nil), CodingKeys(stringValue: "swupdate", intValue: nil), CodingKeys(stringValue: "lastinstall", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))) 从外观上看,您的解决方案应该可以工作... 嗨,Sam,这是一个小小的更新。事实上正如@vadian 所说。 lastinstall 是一个字符串,而不是一个日期。还有一个关键,就是速度不对。所以我想这毕竟是结构的问题......我会更新帖子。谢谢! 很高兴听到这个消息!感谢更新,我去看看【参考方案3】:

如果您只对根字典的值感兴趣,请添加自定义初始化程序

struct LightsObject: Decodable 
    let lights: [LightInfo]
    
    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()
        let dictionary = try container.decode([String:LightInfo].self)
        lights = Array(dictionary.values)
    

解码

let result = try JSONDecoder().decode(LightsObject.self, from: data)
completionHandler(result.lights, nil)

它返回一个LightInfo 对象的数组,您可以声明

@Binding var lights: [LightInfo]

当然,完成处理程序类型必须是([LightInfo]?, Error?) -> Void。无论如何考虑使用更方便的Result类型

【讨论】:

以上是关于如何在 Swift 中使用动态键(在根级别)解码此 JSON?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 4 中为 JSON 编写可解码,其中键是动态的?

如何在 Swift 4 的可解码协议中使用自定义键?

如何在根级别禁用导入的模块日志记录

如何使用 SWIFT 在 plist 中访问数组及其对象?

在 swift 中对动态键和动态对象使用 Codable

如何在 Swift 中解码具有许多唯一键的嵌套 JSON?