Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 966
Joshua Breeden Avatar asked Jun 23 '17 07:06

Joshua Breeden


4 Answers

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.

like image 95
Rob Napier Avatar answered Nov 12 '22 10:11

Rob Napier


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)
like image 38
Ollie Avatar answered Nov 12 '22 10:11

Ollie


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

like image 12
Paulo Mattos Avatar answered Nov 12 '22 10:11

Paulo Mattos


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.

like image 5
Edwin Vermeer Avatar answered Nov 12 '22 12:11

Edwin Vermeer