Swift Tip: Dynamic Coding Keys for JSONDecoder

We’ve been playing around with Swift Standard Library’s encoding and decoding APIs since their official announcement at last year’s WWDC. We’ve used them on production projects and overall, are enjoying all the niceties offered by the type-safe API and auto-synthesized ‘CodingKey’ conformance provided by the compiler. This saves time and a ton of boilerplate code. It also means that we’re able to provide a custom ‘CodingKey’ implementation whenever necessary. Typically, this is done by using a String- or Int-backed enum. However, this approach can be a bit awkward and inflexible to use when interfacing with certain kinds of JSON responses.

Here’s what we mean.

{   "ok": true,   "warning": "something_problematic",   "user": {             "name": "Prolific Engineer",             "userName": "C0de1sGr8t"           }}

Say you have an API endpoint that returns responses formatted in a manner similar to Slack’s and the root-level object contains keys that each map to different types of values. In this specific example, none of the value types are the same: one is a Bool, another a String and the third a nested object.

If you want to drill down into the JSON response and decode only a user object, you will have to implement your own version of Decodable’s init(from: Decoder) function.

struct User: Decodable {    let name: String
let userName: String
enum TopLevelCodingKeys: String, CodingKey {
case user
}
enum UserCodingKeys: String, CodingKey {
case name
case userName
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TopLevelCodingKeys.self)
let userContainer = try container.nestedContainer(keyedBy: UserCodingKeys.self, forKey: .user)
name = try userContainer.decode(String.self, forKey: .name)
userName = try userContainer.decode(String.self, forKey: .userName)
}
}

This solution works. Nonetheless, it breaks down if you ever need to decode User from a different endpoint with a different root-level key. You will have to change your init(from: Decoder) implementation to accommodate this. And what if there is a third endpoint where the response includes the user object at the root-level, no keys required? This predicament is compounded multiple times over the course of a project.

After poking around the web on Stack Overflow and blogs, the workaround we’ve come up with is to create a generic dummy type that represents the object at the top-level. This type will basically create a key based on the name passed into the decoder and decode your type using this key.

extension JSONDecoder {    func decode<T: Decodable>(_ type: T.Type, from data: Data, keyedBy key: String?) throws -> T {
if let key = key {
// Pass the top level key to the decoder
userInfo[.jsonDecoderRootKeyName] = key
let root = try decode(DecodableRoot<T>.self, from: data)
return root.value
} else {
return try decode(type, from: data)
}
}
}
extension CodingUserInfoKey { static let jsonDecoderRootKeyName = CodingUserInfoKey(rawValue: "rootKeyName")!}struct DecodableRoot<T>: Decodable where T: Decodable {

private struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
static func key(named name: String) -> CodingKeys? {
return CodingKeys(stringValue: name)
}
} let value: T init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard
let keyName = decoder.userInfo[.jsonDecoderRootKeyName] as? String,
let key = CodingKeys.key(named: keyName) else {
throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: “Value not found at root level.”))
}

value = try container.decode(T.self, forKey: key)
}
}

Now, with this solution, whenever you encounter a response with a different root-level key, all you have to do is pass that key into the extension function, decode(_:from:keyedBy:). You even get to keep your type concise and remove the custom Decodable implementation!

A full playground with usage examples is available as a Gist here. If you have come across this issue and solved it in a manner different from this, we’d love to hear from you. Feel free to contact us at engineeringblog@prolificinteractive.com.

--

--

Engineering @ Prolific Interactive
Prolific Interactive

Our team partners with leading brands to create incredible mobile products. Here, we share ideas about engineering, mobile, and tech culture.