3

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

user9041624
  • 246
  • 4
  • 14

2 Answers2

8

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"]
user9041624
  • 246
  • 4
  • 14
  • After hours of searching, this finally helped me! The one thing I don't understand is the following line: `if keyedBy.init(stringValue: key.stringValue) == nil`. Why does this return nil? – Sami May 10 '20 at 20:55
1

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • And what if the unknown data is not always of the type `[String:String]`? – Santhosh R Dec 02 '17 at 17:40
  • That is a different issue, which has also been dealt with thoroughly here on Stack Overflow. – matt Dec 02 '17 at 17:49
  • Thanks for the responses. I reviewed [swift-4-decodable-with-keys-not-known-until-decoding-time](https://stackoverflow.com/questions/45598461/swift-4-decodable-with-keys-not-known-until-decoding-time) and code sample by @matt works fine. It's when combining **known keys** and **unknown keys** in a single parse/decoding operation when the implementation gets tricky, because you want to decode the container twice... Or decode as a generic approach, and _manually_ assign the known keys dropping the remainder unknown keys into the dictionary `[String: String]` – user9041624 Dec 03 '17 at 00:36
  • You go in twice, once with the known keys, as you already showed, and once again diving for the unknown keys, as I did. Do I need to spell the whole thing out? – matt Dec 03 '17 at 04:47
  • Can someone provide link of SO question regarding known keys but unknown data type? – Martheli Dec 30 '17 at 05:13