45

I'm using the Swift 4 Codable protocol with JSON data. My data is formatted such that there is a single key at the root level with an object value containing the properties I need, such as:

{
  "user": {
    "id": 1,
    "username": "jdoe"
  }
}

I have a User struct that can decode the user key:

struct User: Codable {
  let id: Int
  let username: String
}

Since id and username are properties of user, not at the root level, I needed to make a wrapper type like so:

struct UserWrapper: Codable {
  let user: User
}

I can then decode the JSON via the UserWrapper, and the User is decoded also. It seems like a lot of redundant code since I'll need an extra wrapper on every type I have. Is there a way to avoid this wrapper pattern or a more correct/elegant way of handling this situation?

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
Joshua Breeden
  • 1,844
  • 1
  • 16
  • 29
  • I didn't go deep in the `Codable` protocol yet, but I think that the quickest way is to directly initialize the `User` object with the internal dictionary already. Can you take the `userDicitonary` out of the user field by accessing it from the dictionary? – Luca D'Alberti Jun 23 '17 at 07:38
  • 1
    If your JSON only contains data for a single user, do you really need the user key and the user dictionary as the value? Wouldn't it be enough to just have the user dictionary? Shouldn't the context indicate that the JSON describes a user? – Palle Jun 23 '17 at 13:48
  • 1
    @Palle Unfortunately, you don't always get to choose the format of the data you need to handle. – Joshua Breeden Jun 25 '17 at 00:48

4 Answers4

52

Ollie's answer is definitely the best way to go for this case, but it does push some knowledge into the caller, which may be undesirable. It also isn't very flexible. I still think it's a great answer and exactly what you want here, but this is a nice simple example to explore custom structural encoding.

How can we make this work correctly:

let user = try? JSONDecoder().decode(User.self, from: json)

We can't use the default conformances anymore. We have to build our own decoder. That's slightly tedious, but not difficult. First, we need to encode the structure into CodingKeys:

struct User {
    let id: Int
    let username: String

    enum CodingKeys: String, CodingKey {
        case user // The top level "user" key
    }

    // The keys inside of the "user" object
    enum UserKeys: String, CodingKey {
        case id
        case username
    }
}

With that, we can decode User by hand by pulling out the nested container:

extension User: Decodable {
    init(from decoder: Decoder) throws {

        // Extract the top-level values ("user")
        let values = try decoder.container(keyedBy: CodingKeys.self)

        // Extract the user object as a nested container
        let user = try values.nestedContainer(keyedBy: UserKeys.self, forKey: .user)

        // Extract each property from the nested container
        id = try user.decode(Int.self, forKey: .id)
        username = try user.decode(String.self, forKey: .username)
    }
}

But I'd absolutely do it Ollie's way for this problem.

For much more on this see Encoding and Decoding Custom Types.

Mazen Kasser
  • 3,559
  • 2
  • 25
  • 35
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    Was searching for something else, but this answer showed me nestedContainer which was the solution to my problem! Thanks for that!!! – Nuno Gonçalves Dec 29 '17 at 17:12
  • Great answer for more complicated jsons! – Roi Mulia Jan 23 '18 at 19:58
  • Hey Rob, I've published a question about multiple containers, would love to hear your thoughts. https://stackoverflow.com/questions/48410909/swift-4-decodable-multiple-containers Thank you! – Roi Mulia Jan 23 '18 at 21:21
48

You could decode using a dictionary: user combination then extract out the user object. e.g.

struct User: Codable {
    let id: Int
    let username: String
}

let decoder = JSONDecoder()
let userDictionary = try decoder.decode([String: User].self, from: jsonData)
Ollie
  • 1,926
  • 1
  • 20
  • 35
  • 1
    Nice solution! It goes to show how flexible these decoding/encoding Swift 4 APIs really are ;) – Paulo Mattos Jun 23 '17 at 14:07
  • 1
    Very clever! My mind was on classes and structs so I wouldn't have thought to make it a dictionary! I marked this solution correct because it is probably the most generally useful answer, but Paulo and Rob's solution is very good as well. A bit more verbose, but more self-contained. Thanks! – Joshua Breeden Jun 23 '17 at 18:25
  • @JoshuaBreeden By the way, did Ollie solution worked as expected? I'm still running Xcode 8 so I couldn't test it myself ;( – Paulo Mattos Jun 23 '17 at 22:28
  • 1
    @PauloMattos, yes, it works great. The only caveat is that, like Rob's answer mentions, as the caller you must know the dictionary key used in the JSON to extract the User object. – Joshua Breeden Jun 23 '17 at 23:42
  • 3
    Is there a way to use this solution for encoding? – Scott Fister Jul 12 '17 at 22:57
  • I don't understand at all how this automatically gets the dictionary inside "user" - nowhere in the code does it say "user". I want to use this but I'm really confused. In my case all API responses have a root object called "data" that in this case is an array. – nickdnk Mar 09 '18 at 16:26
  • @nickdnk if there weren't a `user` key at the root and the JSON is only `{"id": 1, "username": "jdoe" }`, you should have used `let userDictionary = try decoder.decode(User.self, from: jsonData)` to decode it. But it has an extra `{"user" : ...}` which is a dictionary (or `[String:User]`), so you use the provided code (which gives you a **dictionary**) and simply access the `user` key to get your `User` instance. – Alejandro Iván May 14 '18 at 16:50
  • This works but what if there's more than one root object? I get errors in my code. – Van Du Tran Mar 25 '22 at 20:43
12

Of course, you can always implement your own custom decoding/encoding — but for this simple scenario your wrapper type is a much better solution IMO ;)

For comparison, the custom decoding would look like this:

struct User {
    var id: Int
    var username: String

    enum CodingKeys: String, CodingKey {
        case user
    }

    enum UserKeys: String, CodingKey {
        case id, username
    }
}

extension User: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        let user = try values.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
        self.id = try user.decode(Int.self, forKey: .id)
        self.username = try user.decode(String.self, forKey: .username)
    }
}

and you still to conform to the Encodable protocol if you want to support encoding as well. As I said before, your simple UserWrapper is much easier ;)

Mazen Kasser
  • 3,559
  • 2
  • 25
  • 35
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
5

I created a helper extension for Codable that will make things like this easier.

see https://github.com/evermeer/Stuff#codable

With that you can create an instance of your user object like this:

    let v = User(json: json, keyPath: "user")

You don't have to change anything in your original User struct and you don't need a wrapper.

Edwin Vermeer
  • 13,017
  • 2
  • 34
  • 58