Reputation: 266
I'm trying to decode the following JSON in Swift 4:
{
"token":"RdJY3RuB4BuFdq8pL36w",
"permission":"accounts, users",
"timout_in":600,
"issuer": "Some Corp",
"display_name":"John Doe",
"device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"
}
The problem is, the last 2 elements (display_name
and device_id
) in the JSON may or may not exist or the elements could be named something entirely different but still unknown, i.e "fred": "worker", "hours" : 8
So what I'm trying to achieve is decode what IS known, i.e token
, permission
, timeout_in
and issuer
and any other elements (display_name
, device_id
etc) place them into a dictionary.
My structure looks like this:
struct AccessInfo : Decodable
{
let token: String
let permission: [String]
let timeout: Int
let issuer: String
let additionalData: [String: Any]
private enum CodingKeys: String, CodingKey
{
case token
case permission
case timeout = "timeout_in"
case issuer
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
token = container.decode(String.self, forKey: .token)
permission = try container.decodeIfPresent(String.self, forKey: .permission).components(separatedBy: ",")
timeout = try container.decode(Int.self, forKey: . timeout)
issuer = container.decode(String.self, forKey: .issuer)
// This is where I'm stuck, how do I add the remaining
// unknown JSON elements into additionalData?
}
}
// Calling code, breviated for clarity
let decoder = JSONDecoder()
let accessInfo = try decoder.decode(AccessInfo.self, from: data!)
Being able to decode a parts of a known structure where the JSON could contain dynamic info as well is where I'm at if anyone could provide some guidance.
Thanks
Upvotes: 3
Views: 3715
Reputation: 266
Inspired by @matt comments, here is the full sample I've gone with. I extended the KeyedDecodingContainer
to decode the unknown keys and provide a parameter to filter out known CodingKeys
.
Sample JSON
{
"token":"RdJY3RuB4BuFdq8pL36w",
"permission":"accounts, users",
"timout_in":600,
"issuer": "Some Corp",
"display_name":"John Doe",
"device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"
}
Swift structs
struct AccessInfo : Decodable
{
let token: String
let permission: [String]
let timeout: Int
let issuer: String
let additionalData: [String: Any]
private enum CodingKeys: String, CodingKey
{
case token
case permission
case timeout = "timeout_in"
case issuer
}
public init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: CodingKeys.self)
token = container.decode(String.self, forKey: .token)
permission = try container.decode(String.self, forKey: .permission).components(separatedBy: ",")
timeout = try container.decode(Int.self, forKey: . timeout)
issuer = container.decode(String.self, forKey: .issuer)
// Additional data decoding
let container2 = try decoder.container(keyedBy: AdditionalDataCodingKeys.self)
self.additionalData = container2. decodeUnknownKeyValues(exclude: CodingKeys.self)
}
}
private struct AdditionalDataCodingKeys: CodingKey
{
var stringValue: String
init?(stringValue: String)
{
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int)
{
return nil
}
}
KeyedDecodingContainer Extension
extension KeyedDecodingContainer where Key == AdditionalDataCodingKeys
{
func decodeUnknownKeyValues<T: CodingKey>(exclude keyedBy: T.Type) -> [String: Any]
{
var data = [String: Any]()
for key in allKeys
{
if keyedBy.init(stringValue: key.stringValue) == nil
{
if let value = try? decode(String.self, forKey: key)
{
data[key.stringValue] = value
}
else if let value = try? decode(Bool.self, forKey: key)
{
data[key.stringValue] = value
}
else if let value = try? decode(Int.self, forKey: key)
{
data[key.stringValue] = value
}
else if let value = try? decode(Double.self, forKey: key)
{
data[key.stringValue] = value
}
else if let value = try? decode(Float.self, forKey: key)
{
data[key.stringValue] = value
}
else
{
NSLog("Key %@ type not supported", key.stringValue)
}
}
}
return data
}
}
Calling code
let decoder = JSONDecoder()
let accessInfo = try decoder.decode(AccessInfo.self, from: data!)
print("Token: \(accessInfo.token)")
print("Permission: \(accessInfo.permission)")
print("Timeout: \(accessInfo.timeout)")
print("Issuer: \(accessInfo.issuer)")
print("Additional Data: \(accessInfo.additionalData)")
Output
Token: RdJY3RuB4BuFdq8pL36w
Permission: ["accounts", "users"]
Timeout: 600
Issuer: "Some Corp"
Additional Data: ["display_name":"John Doe", "device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"]
Upvotes: 8
Reputation: 534950
The question is actually a duplicate of Swift 4 Decodable with keys not known until decoding time. Once you understand the trick about building a rock-bottom minimal CodingKey adopter struct as your coding key, you can use it for any dictionary.
In this instance, you would use the keyed container's allKeys
to get the unknown JSON dictionary keys.
To demonstrate, I will confine myself to just the completely unknown part of the JSON dictionary. Imagine this JSON:
let j = """
{
"display_name":"John Doe",
"device_id":"uuid824fd3c3-0f69-4ee1-979a-e8ab25558421"
}
"""
let jdata = j.data(using: .utf8)!
Presume that we have no idea what's in that dictionary, beyond that the fact that it has String keys and String values. So we want to parse jdata
without knowing anything about what its keys are.
We therefore have a struct consisting of one dictionary property:
struct S {
let stuff : [String:String]
}
The question now is how to parse that JSON into that struct - i.e., how to make that struct conform to Decodable and deal with that JSON.
Here's how:
struct S : Decodable {
let stuff : [String:String]
private struct CK : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let con = try! decoder.container(keyedBy: CK.self)
var d = [String:String]()
for key in con.allKeys {
let value = try! con.decode(String.self, forKey:key)
d[key.stringValue] = value
}
self.stuff = d
}
}
Now we parse:
let s = try! JSONDecoder().decode(S.self, from: jdata)
And we get an S instance whose stuff
is this dictionary:
["device_id": "uuid824fd3c3-0f69-4ee1-979a-e8ab25558421", "display_name": "John Doe"]
And that is the very result we wanted.
Upvotes: 1