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

Joshua Breeden picture Joshua Breeden · Jun 23, 2017 · Viewed 16.2k times · Source

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?

Answer

Rob Napier picture Rob Napier · Jun 23, 2017

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.