Swift 4 Codable; How to decode object with single root-level key

19,442

Solution 1

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)

Solution 2

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.

Solution 3

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

Solution 4

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.

Share:
19,442

Related videos on Youtube

Joshua Breeden
Author by

Joshua Breeden

I love computers and music. I play in a band called meta. I program mostly in C++, but have occasional flings with Javascript.

Updated on June 06, 2022

Comments

  • Joshua Breeden
    Joshua Breeden about 2 years

    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?

    • Luca D'Alberti
      Luca D'Alberti about 7 years
      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?
    • Palle
      Palle about 7 years
      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?
    • Joshua Breeden
      Joshua Breeden about 7 years
      @Palle Unfortunately, you don't always get to choose the format of the data you need to handle.
  • Paulo Mattos
    Paulo Mattos about 7 years
    Nice solution! It goes to show how flexible these decoding/encoding Swift 4 APIs really are ;)
  • Joshua Breeden
    Joshua Breeden about 7 years
    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!
  • Paulo Mattos
    Paulo Mattos about 7 years
    @JoshuaBreeden By the way, did Ollie solution worked as expected? I'm still running Xcode 8 so I couldn't test it myself ;(
  • Joshua Breeden
    Joshua Breeden about 7 years
    @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.
  • Scott Fister
    Scott Fister almost 7 years
    Is there a way to use this solution for encoding?
  • Nuno Gonçalves
    Nuno Gonçalves over 6 years
    Was searching for something else, but this answer showed me nestedContainer which was the solution to my problem! Thanks for that!!!
  • Roi Mulia
    Roi Mulia over 6 years
    Great answer for more complicated jsons!
  • Roi Mulia
    Roi Mulia over 6 years
    Hey Rob, I've published a question about multiple containers, would love to hear your thoughts. stackoverflow.com/questions/48410909/… Thank you!
  • nickdnk
    nickdnk over 6 years
    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.
  • GameLoading
    GameLoading about 6 years
    Excellent Answer!
  • Alejandro Iván
    Alejandro Iván about 6 years
    @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.
  • Van Du Tran
    Van Du Tran over 2 years
    This works but what if there's more than one root object? I get errors in my code.