1

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.

Doug Smith
  • 29,668
  • 57
  • 204
  • 388
  • So you essentially have a `Dog` type, which has a `name` property and you want to decode a single `[Dog]` array from the JSON response in your question? – Dávid Pásztor May 30 '18 at 15:03
  • @DávidPásztor Sorry that was unclear. I'll update the question – Doug Smith May 30 '18 at 15:05
  • 3
    Sometimes it's simpler to use `JSONSerialization`. It worked for years before `Codable` came along. – rmaddy May 30 '18 at 15:07
  • @rmaddy I know but I'm trying to understand this Swift stuff – Doug Smith May 30 '18 at 15:09
  • @matt What do you mean? This is very similar to proprietary JSON from the company I work at that I can't change. If you have a different Swift representation of the data I'm all ears – Doug Smith May 30 '18 at 15:24

3 Answers3

2

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)
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • Doesn't seem to work I get `keyNotFound(CodingKeys(stringValue: "author", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), _DictionaryCodingKey(stringValue: "data", intValue: nil), _DictionaryCodingKey(stringValue: "children", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"author\", intValue: nil) (\"author\").", underlyingError: nil))` from the printed `error`. – Doug Smith May 30 '18 at 16:00
  • @DougSmith I've simply copy pasted the JSON from your question and it runs just fine. That error message is especially weird since there shouldn't be an `author` key in the structs or JSON. Are you sure you copied this code exactly and used the JSON from the question? – Dávid Pásztor May 30 '18 at 16:12
1

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.

Joshua Smith
  • 6,561
  • 1
  • 30
  • 28
1

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141