简体   繁体   中英

How to use Swift Codable with a nested JSON structure and unknown keys

I have a nested json structure. I know the top level keys (objects) but those may or may not be present for each fetch. Each key within the objects (and the ones nested inside those) is unknown. They are dynamic.

I have tried for hours to use Codable only to use JSONSerialization instead. Before I completely lose hope, I wanted to see if anyone has a solution for this.

Here is an example of what my JSON looks like:

var jsonString =
"""
{
    "someNumbers": {
        "22": 6,
        "22626": 0
    },
    "someNestedAny": {
        "61": {
            "browser": 2
        },
        "8310": {
            "desktop": 2
        }
    },
    "someNestedArray": {
        "49": ["Chrome"],
        "50": ["Mac OS X"],
        "51": ["Mac desktop"],
        "52": ["browser"],
        "53": ["Chrome"]
    }
}
""" 

The key value pairs within each top level object (someNumbers, someNestedAny, and someNestedArray are dynamic. The keys/values within those objects are also dynamic...and so on. Each top level object is also optional.

I have tried many things, but these are what have looked the most promising (neither of them worked, tho)

  1.  struct TopLevel: Decodable { var someNumbers: SomeNumbers? var someNestedAny: SomeNestedAny? var someNestedArray: SomeObjectFromNestedAny? } struct SomeNumbers: Decodable { var key: String var value: Int } struct SomeNestedAny: Decodable { var key: String var value: SomeObjectFromNestedAny } struct SomeObjectFromNestedAny: Decodable { var key: String var value: Int } struct SomeNestedArray: Decodable { var key: String var value: [String] } let data = jsonString.data(using: .utf8)! do { let result = try JSONDecoder().decode(TopLevel.self, from: data) print(result) } catch { print(error) } 

Output: keyNotFound(CodingKeys(stringValue: "key", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "someNumbers", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \\"key\\", intValue: nil) (\\"key\\").", underlyingError: nil))

  1.  struct TopLevel: Decodable { var someNumbers: SomeNumbers? var someNestedAny: SomeNestedAny? var someNestedArray: SomeObjectFromNestedAny? } struct SomeNumbers: Decodable { public var numbersObject: [String: NumberKeys] public struct NumberKeys: Decodable { public let key: String public let value: Int } private struct NumberCodingKeys: CodingKey { var stringValue: String init?(stringValue: String) { self.stringValue = stringValue } var intValue: Int? init?(intValue: Int) { return nil } } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: NumberCodingKeys.self) self.numbersObject = [String: NumberKeys]() for key in container.allKeys { let value = try container.decode(NumberKeys.self, forKey: NumberCodingKeys(stringValue: key.stringValue)!) self.numbersObject[key.stringValue] = value } } } struct SomeNestedAny: Decodable { // was going to do the same thing as above - but it didn't work var key: String var value: SomeObjectFromNestedAny } struct SomeObjectFromNestedAny: Decodable { // was going to do the same thing as above - but it didn't work var key: String var value: Int } struct SomeNestedArray: Decodable { // was going to do the same thing as above - but it didn't work var key: String var value: [String] } let data = jsonString.data(using: .utf8)! do { let result = try JSONDecoder().decode(TopLevel.self, from: data) print(result) } catch { print(error) } 

Output: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "someNumbers", intValue: nil), NumberCodingKeys(stringValue: "22626", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found a number instead.", underlyingError: nil))

Here is what I have working now, but is really gross:

let data = jsonString.data(using: .utf8)
let json = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
if let numbers = json["someNumbers"] as? [String:Any] {
    for (key, value) in numbers {
        print("key: \(key), value: \(value)")
    }
}
if let anys = json["someNestedAny"] as? [String: Any] {
    //print("tkey: \(anys)")
    for (key, value) in anys {
        //print("tkey2: \(key), tvalue: \(value)")
        if let value = value as? [String: Any] {
            let anyData = try JSONSerialization.data(withJSONObject: value, options: .prettyPrinted)
            let anyJson = try JSONSerialization.jsonObject(with: anyData) as! [String: Any]
            for (key2, value2) in anyJson {
                print("key: \(key), object-key: \(key2), object-value: \(value2)")
            }
        }
    }
}
if let arrays = json["someNestedArray"] as? [String: Any] {
    for (key, value) in arrays {
        print("key: \(key), value: \(value)")
    }
}

Thanks in advance!

It's hard to tell if this solves your exact use case since I'm not sure what the purpose of the data is, and how you're anticipating using it, but both of the below solutions correctly decode your JSON into a more usable Swift object.

The simplest way is to model it exactly as the data structure you presented. For example it looks like someNumbers is an optional dictionary keyed by String , with Int values: [String: Int]? .

struct TopLevel: Decodable {
  var someNumbers: [String: Int]?
  var someNestedAny: [String: [String: Int]]?
  var someNestedArray: [String: [String]]?
}

For a little more readability when passing objects around you can throw in some type aliases and it becomes

typealias SomeNumbers = [String: Int]
typealias SomeNestedAny = [String: [String: Int]]
typealias SomeNestedArray = [String: [String]]

struct TopLevel: Decodable {
  var someNumbers: SomeNumbers?
  var someNestedAny: SomeNestedAny?
  var someNestedArray: SomeNestedArray?
}

To get useful things out you'll then need to call things like

topLevel.someNumbers?["22"]                 // 6
topLevel.someNestedAny?["8310"]             // ["desktop": 2]
topLevel.someNestedAny?["8310"]?["desktop"] // 2
topLevel.someNestedArray?["52"]             // ["browser"]
topLevel.someNestedArray?["52"]?[0]         // "browser"

Or depending on your needs it may make more sense to loop through things

topLevel.someNestedAny?
  .forEach { item in
    print("|- \(item.key)")
    item.value.forEach { any in
      print("|  |- \(any.key)")
      print("|  |  |- \(any.value)")
    }
}

// |- 61
// |  |- browser
// |  |  |- 2
// |- 8310
// |  |- desktop
// |  |  |- 2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM