-
-
Save haikusw/0d729dee23901f5e75905434e70581a6 to your computer and use it in GitHub Desktop.
Decode JSON at root level based on a key
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
// Usage | |
import Foundation | |
// Slack-style API responses with error information at the top-level. | |
// https://api.slack.com/web#responses | |
let data = """ | |
{ | |
"ok": true, | |
"warning": "something_problematic", | |
"user": { | |
"name": "Htin Linn", | |
"userName": "Litt" | |
} | |
} | |
""".data(using: .utf8)! | |
struct User: Decodable { | |
let name: String | |
let userName: String | |
} | |
let jsonDecoder = JSONDecoder() | |
do { | |
// This doesn't work because user object is not at the top level. | |
// let _ = try jsonDecoder.decode(User.self, from: data) | |
// This doesn't work because `Any` is not `Decodable`. | |
// let _ = try jsonDecoder.decode([String: Any].self, from: data) | |
// This doesn't work because the decoder will try to decode every single key at the top-level as a `User` type. | |
// let _ = try jsonDecoder.decode([String: User].self, from: data) | |
// But, this works! | |
let user = try jsonDecoder.decode(User.self, from: data, keyedBy: "user") | |
print(user) | |
} catch { | |
print(error) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment