Doug Smith
Doug Smith

Reputation: 29326

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.

Upvotes: 1

Views: 1144

Answers (3)

matt
matt

Reputation: 535556

Use intermediate structs to dumpster-dive and collect the desired data, and then dispose of them.

So, start with the Dog struct, declared at top level:

struct Dog : Decodable { let name : String }

In your actual code, make temporary local structs to wrap it and decode the JSON:

struct TheChildren : Decodable { let children : [Dog] }
struct TheData : Decodable { let data : TheChildren }
let arr = try! JSONDecoder().decode([TheData].self, from: yourJSONdata)

Now just pull out the desired Dogs:

let dogs = arr.map {$0.data.children}
/*
[[Dog(name: "Ralph"), Dog(name: "Woofer")], 
 [Dog(name: "Spot"), Dog(name: "Trevor")]]
*/

That's an array of arrays of Dogs, so both "arrays are maintained separately" in that they are separate elements of the result array. That seems a perfectly reasonable representation.

Now, if you want to further stuff that info into a new struct, fine. It isn't going to be the same as your posited Result struct, because the names dogs1 and dogs2 appear nowhere in the data, and you can't make up a property name at runtime (well, in Swift 4.2 you sort of can, but that's another story). But the point is, you've got the Dog data, easily, and with no extra material. And there's no real reason why accessing the first array under a name dogs1 is better than getting it by index as dogs[0]; indeed, the latter is actually better. Ending a property name with an index number is always a Bad Smell suggesting that what you really needed is a collection of some sort.

Upvotes: 1

David Pasztor
David Pasztor

Reputation: 54745

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

Upvotes: 2

Joshua Smith
Joshua Smith

Reputation: 6631

So, the short answer is: You can't and the long answer is longer.

tl;dr

https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

One way to skin this is by starting with an intermediate representation of your structures. Something like this:

struct Intermediate: Codable { struct Dog: Codable { let name: String } struct Children: Codable { let children: [Dog] } let data: Children }

and then you can transform that into your Result struct. And you can transform your Result struct into an intermediate one for serialization. That lets you escape more complicated use of conding keys and encoders. You can keep the intermediate representations private in your module if you don't want anyone to poke at it.

Upvotes: 1

Related Questions