0

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

3
  • 2
    app.quicktype.io Commented Sep 15, 2021 at 19:58
  • 1
    The error is pretty clear. Please read the error message carefully. The value for key lastinstall is a String, not a Date. You need an appropriate date decoding strategy to decode the value directly to Date Commented Sep 15, 2021 at 20:02
  • If the solutions do work, please vote them up and select the best one as the solution. Commented Sep 16, 2021 at 7:13

3 Answers 3

1

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
Sign up to request clarification or add additional context in comments.

2 Comments

Hi Rob! Thanks for the suggestion. I never knew, there was another level, where I could retrieve the values... However, the compiler now complains about the type. Would this be Dictionary<String, LightObject>.Values? Seems a bit strange.
Yes, this would be Dictionary<String, LightObject>.Values. If you want an Array, you can convert it with Array(lights). The Values type is lazy; it doesn't force a copy. When you wrap it into an Array, then it has to actually copy things (if that's what you want).
1

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

3 Comments

Hi Sam! So I tried your solution. Had to change the LightsObject to a class, since Swift was complaining about the init function. When I try to decode it now, I still get a similar error: 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))) From the looks of it your solution should work...
Hi Sam, so a small update. It was in fact as @vadian said. The lastinstall was a String, not a Date. There was also a key, that was speed incorrectly. So I guess it was a problem with the struct after all... I'll update the post. Thanks!
Glad to hear it! Thanks for the updates, I will check it out
0
struct DynamicCodingKey: CodingKey {
var stringValue: String
var intValue: Int?

init?(stringValue: String) {
    self.stringValue = stringValue
    self.intValue = nil
}

init?(intValue: Int) {
    self.stringValue = "\(intValue)"
    self.intValue = intValue
}

init(stringValue: String, intValue: Int?) {
    self.stringValue = stringValue
    self.intValue = intValue
}
}


struct EmailData: Codable {
let email: String
let lastEnumeratedTime: String
let verifySelection: String
let enumerationTime: String
let enumerationFrequency: String

enum CodingKeys: String, CodingKey {
    case email
    case lastEnumeratedTime = "LastEnumeratedTime"
    case verifySelection = "VerifySelection"
    case enumerationTime = "EnumerationTime"
    case enumerationFrequency = "EnumerationFrequency"
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: DynamicCodingKey.self)
    email = container.allKeys.first!.stringValue
    let emailContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: DynamicCodingKey(stringValue: email)!)
    lastEnumeratedTime = try emailContainer.decode(String.self, forKey: .lastEnumeratedTime)
    verifySelection = try emailContainer.decode(String.self, forKey: .verifySelection)
    enumerationTime = try emailContainer.decode(String.self, forKey: .enumerationTime)
    enumerationFrequency = try emailContainer.decode(String.self, forKey: .enumerationFrequency)
 }
}

Here's an example usage of this Codable model:

let json = """
{
 "[email protected]": {
"LastEnumeratedTime": "20-Apr-2023 at 9:52:26 AM",
"VerifySelection": "1",
"EnumerationTime": "20-Apr-2023 at 11:19:33 AM",
"EnumerationFrequency": "Days"
 }
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let emailData = try! decoder.decode(EmailData.self, from: json)

print(emailData.email) // Output: "[email protected]"
print(emailData.lastEnumeratedTime) // Output: "20-Apr-2023 at 9:52:26 AM"
print(emailData.verifySelection) // Output: "1"
print(emailData.enumerationTime) // Output: "20-Apr-2023 at 11:19:33 AM"
print(emailData.enumerationFrequency) // Output: "Days"

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.