Search code examples
jsonswiftstructswift4jsondecoder

How do I decode JSON in Swift where it's an array with double-nested items?


Say the JSON looks like this:

[
    {
      "data": {
        "children": [
          {
            "name": "Ralph"
          },
          {
            "name": "Woofer"
          }
        ]
      }
    },
    {
      "data": {
        "children": [
          {
            "name": "Spot"
          },
          {
            "name": "Trevor"
          }
        ]
      }
    }
]

Where you have this very weird structure where there the root item is an array, with two objects, and each of those two objects is an array of Dog dictionaries.

But the problem is that the Dog array is two keys in! You have to go through data and children to get to it. I saw this answer that depicts doing it with a single key deep, but I can't seem to reproduce the result when it's nested two deep.

I want the result to be (as weird as it seems) something like this, where both lists are maintained separately:

struct Result: Codable {
    let dogs1: [Dog]
    let dogs2: [Dog]
}

I know I need a custom initializer/decoder, but I'm very unsure of how to access it.


Solution

  • You can decode that JSON without having to introduce intermediate structs while keeping type safety by decoding the outer Dictionary whose only key is data as a nested Dictionary of type [String:[String:[Dog]]], which is pretty messy, but works since you only have 2 nested layers and single keys in the outer dictionaries.

    struct Dog: Codable {
        let name:String
    }
    
    struct Result: Codable {
        let dogs1: [Dog]
        let dogs2: [Dog]
    
        enum DogJSONErrors: String, Error {
            case invalidNumberOfContainers
            case noChildrenContainer
        }
    
        init(from decoder: Decoder) throws {
            var containersArray = try decoder.unkeyedContainer()
            guard containersArray.count == 2 else { throw DogJSONErrors.invalidNumberOfContainers}
            let dogsContainer1 = try containersArray.decode([String:[String:[Dog]]].self)
            let dogsContainer2 = try containersArray.decode([String:[String:[Dog]]].self)
            guard let dogs1 = dogsContainer1["data"]?["children"], let dogs2 = dogsContainer2["data"]?["children"] else { throw DogJSONErrors.noChildrenContainer}
            self.dogs1 = dogs1
            self.dogs2 = dogs2
        }
    }
    

    Then you can simply decode a Result instance like this:

    do {
        let dogResults = try JSONDecoder().decode(Result.self, from: dogsJSONString.data(using: .utf8)!)
        print(dogResults.dogs1,dogResults.dogs2)
    } catch {
        print(error)
    }