-1

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?

Anton Unt
  • 1,835
  • 1
  • 21
  • 47
  • 1
    You must decode from the root object/top level. – Joakim Danielson Jul 22 '22 at 19:49
  • 3
    ... and providing the *boilerplate type* is much more efficient than the *hacky* way. – vadian Jul 22 '22 at 20:44
  • 2
    You could decode it as `[String:[Group]].self` and the access the `Groups` element of the dictionary, but why not just do it the right way? It gives simpler code and app.QuickType.io can write the structs for you – Paulw11 Jul 22 '22 at 21:53
  • 1
    Note, there should be no `name` in your `GroupsResponse` because there is no name in the json data. There is however a `"totalCount": 41`. Similarly, there is no `groups` in your json data, there is `Groups`, so change your `GroupsResponse` accordingly. It seems you are just making things up as you type the post, and not showing us the code (and data) you are really using. Take @vadian advice and use a proper data model instead of hacking away using poor code practice. – workingdog support Ukraine Jul 23 '22 at 00:29
  • @workingdogsupportUkraine I've corrected the issues. I'd still like to find out how to do it without creating response types even if it is for education. – Anton Unt Jul 23 '22 at 07:05
  • Implement `init(from decoder` and use `nestedContainer`. But – again – this is more code to write than the tiny root struct. – vadian Jul 23 '22 at 07:08
  • your edits are still not correct. There should be no `totalCount` in your `Group` struct. It is in `GroupsResponse`. Your models do not match your json data. – workingdog support Ukraine Jul 23 '22 at 07:52
  • Your boilerplate type is much easier and readable than the hacky way. – Ankur Lahiry Jul 23 '22 at 12:08

1 Answers1

1

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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610