Swift 4 Codable; How to decode object with single

2019-01-13 18:00发布

问题:

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?

回答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)


回答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.



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



回答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.