如何在 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
【讨论】:
嗨,罗伯!谢谢你的建议。我从来不知道,还有另一个级别,我可以在其中检索值......但是,编译器现在抱怨类型。这会是 DictionaryDictionary<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?的主要内容,如果未能解决你的问题,请参考以下文章