Decoding Dynamic JSON Structure in Swift 4

Dynamic JSON Decoding Swift 4

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"]

Swift Dynamic json Values Reading and Writing

Based on the comments and our chat, this seems to be a question of the right way to construct the Swift models and the JSON object.

Based on your (updated) JSON, you might want to decode your data into a [String: UserDay] - a dictionary with a date string as key and UserDay as a value.

First, WP property in your JSON is just an array of objects (that map to WPData), so it's best to change your UserDay.wp to be [WPData] instead of UserWP:

struct UserDay: Codable {
let mp: UserMP
let wp: [WPData] // <-- changed
}

Second, some of your models' properties don't match directly to what's in JSON because keys-properties mapping is case sensitive. You can explicitly define CodingKeys to map them:

struct UserDay: Codable {
let mp: UserMP
let wp: [WPData]

enum CodingKeys: String, CodingKey {
case mp = "MP", wp = "WP"
}
}

struct UserMP: Codable {
let m: [UserM]
let s: [UserS]

enum CodingKeys: String, CodingKey {
case m = "M", s = "S"
}
}

Now you're ready to decode [String: UserDay]:

let userDays = try JSONDecoder().decoder([String: UserDay].self, from: jsonData)

let userDay = userDays["01/29/2020"]


Of course, working with String instead of Date isn't very convenient. Unfortunately, Dictionary's conformance to Codable only supports Int or String as keys (AFAIK).

So, let's do a manual decoding into a new root object UserData that works with Dates:

struct UserData: Codable {
var userDays: [Date: UserDay]

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dict = try container.decode([String: UserDay].self)

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")

// decode (String, UserDay) pairs into an array of (Date, UserDay)
let pairs = dict.compactMap { (key, value) -> (Date, UserDay)? in
guard let date = dateFormatter.date(from: key) else { return nil }
return (date, value)
}

// uniquing is used just in case there non unique keys
self.userDays = Dictionary(pairs, uniquingKeysWith: {(first, _) in first})
}
}

Now, we can decode into this UserData object:

let userData = try JSONDecoder().decode(UserData.self, from: jsonData)

let todaysData = userData.userDays[Date()]

Swift Codable: Decoding dynamic keys

You're almost there. The JSON you're getting will return a dictionary of [String:Double]. You can then covert that using:

struct ConversionResponseModel: Decodable {
typealias DestinationCurrency = String

let currency : DestinationCurrency
let value : Double

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dict = try container.decode([String:Double].self)
guard let key = dict.keys.first else {
throw NSError(domain: "Decoder", code: 0, userInfo: [:])
}
currency = key
value = dict[key] ?? -1
}
}

Note: taking into account Rob Napier's comment, you could substitute Decimal for Double -- see his comment on the original question for more detail

Parsing Dynamic JSON Model in Swift

You can add some custom decoding logic to try both cases:

struct IceStats: Decodable {
var source: [Source]

enum CodingKeys : CodingKey {
case source
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let elements = try? container.decode([Source].self, forKey: .source) {
source = elements
} else if let element = try? container.decode(Source.self, forKey: .source) {
source = [element]
} else {
throw DecodingError.dataCorruptedError(forKey: .source, in: container, debugDescription: "Must be either a single or multiple sources!")
}
}
}

But this can get really long if you also want to decode other properties in IceStats, because you'll have to manually write the decoding code for those too. To avoid this, you can use a property wrapper:

@propertyWrapper
struct MultipleOrSingle<Element: Decodable>: Decodable {
let wrappedValue: [Element]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let elements = try? container.decode([Element].self) {
wrappedValue = elements
} else if let element = try? container.decode(Element.self) {
wrappedValue = [element]
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Container neither contains a single, nor multiple \(Element.self)!")
}
}
}

// Now you can just do this!
struct IceStats: Decodable {
@MultipleOrSingle
var source: [Source]
}

Trying to decode dynamic JSON response

Your JSON can be decoded into a dictionary of type [String: Int].

If you enter key and value of each entry of the dictionary into a Tag object, you can then sort the array of tags by name. Or you can sort the dictionary keys first, both work.

Catch: it will work only if the order of the JSON is also the order of the keys. It will NOT follow the order if the JSON arrives like this, for example:

{
"tag_name_3": 5,
"tag_name_2": 5,
"tag_name_1": 5,
}

Here's the code (with two options):

    // First way:
// 1) Decode the dictionary
// 2) Sort the dictionary keys
// 3) Iterate, creating a new Tag and appending it to the array
private func decode1() {
let json = "{ \"tag_name_1\": 5, \"tag_name_2\": 5, \"tag_name_3\": 5}"
let data = json.data(using: .utf8)

do {

// Decode into a dictionary
let decoded = try JSONDecoder().decode([String: Int].self, from: data!)
print(decoded.sorted { $0.key < $1.key })

var tags = [Tag]()

// Iterate over a sorted array of keys
decoded.compactMap { $0.key }.sorted().forEach {

// Create the Tag
tags.append(Tag(name: $0, count: decoded[$0] ?? 0))
}
print(tags)
} catch {
print("Oops: something went wrong while decoding, \(error.localizedDescription)")
}
}
    // Second way:
// 1) Decode the dictionary
// 2) Create the Tag objects
// 3) Sort the array by Tag.name
private func decode2() {
let json = "{ \"tag_name_1\": 5, \"tag_name_2\": 5, \"tag_name_3\": 5}"
let data = json.data(using: .utf8)

do {

// Decode into a dictionary
let decoded = try JSONDecoder().decode([String: Int].self, from: data!)
print(decoded.sorted { $0.key < $1.key })

var tags = [Tag]()

// Iterate over the dictionary entries
decoded.forEach {

// Create the Tag
tags.append(Tag(name: $0.key, count: $0.value))
}

// Sort the array by name
tags = tags.sorted { $0.name < $1.name }
print(tags)
} catch {
print("Oops: something went wrong while decoding, \(error.localizedDescription)")
}
}

Swift Codable to parse JSON with dynamic keys

The Codable models that you must use to parse the above JSON data should be like,

Models:

struct StateData: Codable {
var districtData: [String:DistrictData]?
}

struct DistrictData: Codable {
var confirmed: Int?
var lastupdatedtime: String?
var delta: DailyConfirmedData?
}

struct DailyConfirmedData: Codable {
var confirmed: Int?
}

Parsing:

let summary = try JSONDecoder().decode([String:StateData].self, from: data)

Note: There is no need to explicitly create enum CodingKeys if the JSON keys exactly match the properties of the Codable type.



Related Topics



Leave a reply



Submit