Decode JSON array inside a JSON dictionary without creating Boilerplate Types

253 views Asked by At

The JSON:

let jsonString = """
    {
    "groups": [
      {
        "id": "oruoiru",
        "testProp": "rhorir",
        "name": "* C-Level",
        "description": "C-Level"
      },
      {
        "id": "seese",
        "testProp": "seses",
        "name": "CDLevel",
        "description": "CDLevel"
      }
    ],
    "totalCount": 41
    }
    """

Type:

struct Group: Codable {
    var id: String
    var name: String
}

I would like to decode this JSON to only output an array of Group type without having to create boilerplate type like:

struct GroupsResponse: Codable {
        var groups: [Group]
        var totalCount: Int
}

and use:

let data = jsonString.data(using: .utf8)

let decoded = try! JSONDecoder().decode([Group].self, from: data!)

I tried getting the containers from inside the initialiser of the Group type, but the program already crashes outside at the decoding line with Swift.DecodingError.typeMismatch error

One solution that does work is doing something like:

let topLevel = try! JSONSerialization.jsonObject(with: data) as? [String: Any]
let groupsNode = topLevel?["Groups"] as? [[String: Any]]
let groups = try! JSONSerialization.data(withJSONObject: groupsNode!)

let decoded = try! JSONDecoder().decode([Group].self, from: groups)

but this seems very hacky. Is there an elegant way to handle this?

1

There are 1 answers

0
Rob Napier On

You cannot avoid the top level response struct using JSONDecoder. There has to be a type for it to work on. And you can't use Dictionary as the top level object (ie [String: [Group]]), since there's a totalCount field that doesn't have an array of Group. All the comments are correct. Just write the little wrapper. It's one line long:

struct GroupsResponse: Codable { var groups: [Group] }

There's no need to decode fields you don't care about.

But you said "for education," and it's all code, so of course you can replace JSONDecoder with something that can do this. You tried to do that with NSJSONSerialization, but that's extremely clunky. You can also just write your own version of JSONDecoder, and then do it like this:

let decoder = RNJSONDecoder()
let response = try decoder.decode(JSON.self, from: json)
let groups = try decoder.decode([Group].self, from: response.groups)

This avoids any re-encoding (RNJSONDecoder can decode directly from a JSON data structure; it doesn't have to convert it back to Data first). It also requires about 2600 lines of incredibly tedious boilerplate code, mostly copy and pasted out of stdlib. Implementing your own Decoder implementation is obnoxious.

If you wanted to get fancier, you could scan the data for the section that corresponds to the property you want, and decode just that part. While implementing the Decoder protocol is very hard, parsing JSON is quite straight-forward. I'm currently doing a bunch of JSON experiments, so I may try writing that and I'll update this if I do.

But the answers are: "just write the tiny, simple, fast, easy to understand response wrapper," or "replace JSONDecoder with something more powerful."


UPDATE:

I went ahead and built the scanner I mentioned, just to show how it could work. It's still a bit rough, but it allows things like:

let scanner = JSONScanner()
let groupJSON = try scanner.extractData(from: Data(jsonString.utf8), 
                                        forPath: ["groups", 1])
let group = try JSONDecoder().decode(Group.self, from: groupJSON)
XCTAssertEqual(group.id, "seese")

So you can just extract the part of the data you want to parse, and not worry about parsing the rest.