3

I've the following issue, where I don't get the structure right to decode a response from node-red for the status of my sonos players:

[[{"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}]]

My structure & the call to decode looks as follows:

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)

I get the following error in Swift:

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

Any suggestions?

  • Print error, not error.localizedDescription. It will have more infos. You'll see that there is no `sonos` keys. It should be just `let sonos = try JSONDecoder().decode([sonosStatus].self, from: data)` – Larme Jul 09 '20 at 10:52
  • Where are the keys `sonos` and `status` in the JSON? And name structs always with starting uppercase letter. – vadian Jul 09 '20 at 10:53
  • Where do you get the error? Is it thrown by `JSONDecoder().decode`? – Dávid Pásztor Jul 09 '20 at 10:53
  • Thanks, that helped. Changed to let sonos = try JSONDecoder().decode([sonosStatus].self, from: data) – Luca De Blasio Jul 09 '20 at 11:35

3 Answers3

0

One of the the problem is related to urlPort. Your json have both Int and String values for urlPort. You can define custom init to handle that.

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

And also your json is an array of sonosStatus array.

So you need yo modify Response typealias like below:

typealias Response = [[sonosStatus]]
Omer Faruk Ozturk
  • 1,722
  • 13
  • 25
0

You have 2 errors with your code:

  1. The data you are tring to decode is [[sonosStatus]]. so you need to decode as:
let response = try JSONDecoder().decode([[sonosStatus]].self, from: data)
  1. urlPort represents both String and Int. So you need to either correct your JSON format or support both formats with a custom decoder.

You can achieve that with this simple Property wrapper:

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

And then using it like:

struct sonosStatus: Codable {
    let urlHostname: String
    @SomeKindOfInt var urlPort: Int?
    let baseUrl: String
    let sonosName: String
    let uuid: String
    let invisible: Bool
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
-1

You can use JSONSerialization with the .allowFragments reading option.

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

And it was fixed on latest Swift version: https://bugs.swift.org/browse/SR-6163

nghiahoang
  • 538
  • 4
  • 10
  • The answer has nothing to do with the question. If the expected type is array or dictionary `allowFragments` is pointless. And never cast JSON data to an `NS...` collection type in Swift – vadian Jul 09 '20 at 11:07
  • @vadian I think the root cause is the top level of json is an array, not a dictionary. `JSONSerialization` with `allowFragments` solve it. For NSDictionary casting, why we can't do it? – nghiahoang Jul 09 '20 at 11:11
  • No, `allowFragments` is for primitive top level types like `String` or `Int`. In terms of JSON both array and dictionary are objects. And casting to NSDictionary loses the (strong) type information, use always native Seift types. – vadian Jul 09 '20 at 11:20
  • yaaaa, I see ;) – nghiahoang Jul 09 '20 at 11:21