user9041624
user9041624

Reputation: 266

Dynamic JSON Decoding Swift 4

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

Answers (2)

user9041624
user9041624

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

matt
matt

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

Related Questions