Home > Enterprise >  How to decode this JSON with dynamic keys (on the root level) in Swift?
How to decode this JSON with dynamic keys (on the root level) in Swift?

Time:09-16

I am trying to create a small app to control my Hue lights with SwiftUI, but I just can't manage to decode/iterate through this JSON. There are so many threads on how to do this and I have tried tens of them, using CodingKeys, creating custom decoders, and so on, but I just can't seem to get it. So this is the JSON I am getting from my Hue Bridge:

{
    "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"
        },
        ...
    },
    ...
}

So the structure is basically a dynamic key on the root level and the light info, that is the same. I have created the following types:

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
    }
    ...

(it basically continues to include all the variables from the object)

The problem I have now, is that I can't seem to get this into a normal array or dictionary. I would settle for something like {"1":LightInfo, "2", LightInfo}, where I could iterate over, or just a simple [LightInfo, LightInfo, ...], because I may not even need the index.

And then, ideally, I could do something like

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

I have tried creating a custom coding key, implementing the type as Codable, and so on, but I couldn't find a solution, that works for me. I know, there are a lot of threads on this topic, but I feel that my initial setup might be wrong and that's why it's not working.

This is the decoding part:

            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)
                }
            })

I am actually getting the JSON, no problem, but I have not been able to decode it, yet, as I said. The latest error being:

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)))

I have seen some posts, but the JSON they were working with usually had a top-level object, something like

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

So the handling of that was a little different, since I could just create a struct like

struct Object: Decodable {
    var foo: [LightsInfo]
}

which is not possible here :(

Can anybody point me in the right direction? Thanks!

Update:

Thanks guys, the solutions do work. I had some errors in my struct, which are now fixed. (misspelled variable names, optional values and so on). The only thing missing now, is how to loop through the dictionary. If I use

ForEach(lights)...

Swift complains, that it is only supposed to be used for static Dictionaries. I tried doing this

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

but I get this error: Generic struct 'ForEach' requires that '[String : LightInfo]' conform to 'RandomAccessCollection'

Update 2:

So this seems to work:

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)
            }
        }
    }
}

I'll try to clean up the code and optimize it a bit. Open for suggestions ;)

CodePudding user response:

You seem to be almost there. This is a [String:LightInfo]. You just need to decode that (rather than wrapping that up in a LightsObject that doesn't exist in the JSON). You can pull off the values if you don't care about the numbers. It should be like this:

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

CodePudding user response:

I ran into a similar problem myself where I wanted to handle generic keys given. This is how I solved it, adapted to your code.

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)"
    }
}
  • Related