Swift 4 Codable; How to decode object with single root-level key
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.
Related videos on Youtube
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, 2022Comments
-
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 theuser
key:struct User: Codable { let id: Int let username: String }
Since
id
andusername
are properties ofuser
, 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 theUser
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 about 7 yearsI didn't go deep in the
Codable
protocol yet, but I think that the quickest way is to directly initialize theUser
object with the internal dictionary already. Can you take theuserDicitonary
out of the user field by accessing it from the dictionary? -
Palle about 7 yearsIf 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 about 7 years@Palle Unfortunately, you don't always get to choose the format of the data you need to handle.
-
-
Paulo Mattos about 7 yearsNice solution! It goes to show how flexible these decoding/encoding Swift 4 APIs really are ;)
-
Joshua Breeden about 7 yearsVery 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 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 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 almost 7 yearsIs there a way to use this solution for encoding?
-
Nuno Gonçalves over 6 yearsWas searching for something else, but this answer showed me nestedContainer which was the solution to my problem! Thanks for that!!!
-
Roi Mulia over 6 yearsGreat answer for more complicated jsons!
-
Roi Mulia over 6 yearsHey Rob, I've published a question about multiple containers, would love to hear your thoughts. stackoverflow.com/questions/48410909/… Thank you!
-
nickdnk over 6 yearsI 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 about 6 yearsExcellent Answer!
-
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 usedlet 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 theuser
key to get yourUser
instance. -
Van Du Tran over 2 yearsThis works but what if there's more than one root object? I get errors in my code.