How to Get the Nondecoded Attributes from a Decoder Container in Swift 4

How to get the nondecoded attributes from a Decoder container in Swift 4?

You guys keep coming up with novel ways to stress the Swift 4 coding APIs... ;)

A general solution, supporting all value types, might not be possible. But, for primitive types, you can try this:

Create a simple CodingKey type with string-based keys:

struct UnknownCodingKey: CodingKey {
init?(stringValue: String) { self.stringValue = stringValue }
let stringValue: String

init?(intValue: Int) { return nil }
var intValue: Int? { return nil }
}

Then write a general decoding function using the standard KeyedDecodingContainer keyed by the UnknownCodingKey above:

func decodeUnknownKeys(from decoder: Decoder, with knownKeys: Set<String>) throws -> [String: Any] {
let container = try decoder.container(keyedBy: UnknownCodingKey.self)
var unknownKeyValues = [String: Any]()

for key in container.allKeys {
guard !knownKeys.contains(key.stringValue) else { continue }

func decodeUnknownValue<T: Decodable>(_ type: T.Type) -> Bool {
guard let value = try? container.decode(type, forKey: key) else {
return false
}
unknownKeyValues[key.stringValue] = value
return true
}
if decodeUnknownValue(String.self) { continue }
if decodeUnknownValue(Int.self) { continue }
if decodeUnknownValue(Double.self) { continue }
// ...
}
return unknownKeyValues
}

Finally, use the decodeUnknownKeys function to fill your unknownAttributes dictionary:

struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}

let name: String
let unknownAttributes: [String: Any]

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)

let knownKeys = Set(container.allKeys.map { $0.stringValue })
self.unknownAttributes = try decodeUnknownKeys(from: decoder, with: knownKeys)
}
}

A simple test:

let jsonData = """
{
"name": "Foo",
"age": 21,
"token": "ABC",
"rate": 1.234
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
print(person.name)
print(person.unknownAttributes)

prints:

Foo

["age": 21, "token": "ABC", "rate": 1.234]

Swift: Create Decodable model from JSON which contains Dictionary in a Dictionary

In case if you are still looking for a solution, slightly clunky way of doing this could be : (totally got inspired from the excellent answer of this post : How to get the nondecoded attributes from a Decoder container in Swift 4?)

struct UnknownCodingKey: CodingKey {

init?(stringValue: String) { self.stringValue = stringValue }
let stringValue: String

init?(intValue: Int) {return nil }
var intValue: Int? { return nil }
}

struct Team:Codable {

var fotaEnabled:Bool
var version: String
var info : HWVersion?

init(from decoder: Decoder) throws {

self.fotaEnabled = true
self.version = ""

let container = try decoder.container(keyedBy: UnknownCodingKey.self)
for key in container.allKeys {

if let boolValue = try? container.decode(Bool.self, forKey: key) {
self.fotaEnabled = boolValue

} else if let dataValue = try? container.decode(HWVersion.self, forKey: key) {
self.version = key.stringValue
self.info = dataValue

} else {
continue
}
}

}
}

Swift 4 Decodable with keys not known until decoding time

The key is in how you define the CodingKeys property. While it's most commonly an enum it can be anything that conforms to the CodingKey protocol. And to make dynamic keys, you can call a static function:

struct Category: Decodable {
struct Detail: Decodable {
var category: String
var trailerPrice: String
var isFavorite: Bool?
var isWatchlist: Bool?
}

var name: String
var detail: Detail

private struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String

init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }

static let name = CodingKeys.make(key: "categoryName")
static func make(key: String) -> CodingKeys {
return CodingKeys(stringValue: key)!
}
}

init(from coder: Decoder) throws {
let container = try coder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.detail = try container.decode([Detail].self, forKey: .make(key: name)).first!
}
}

Usage:

let jsonData = """
[
{
"categoryName": "Trending",
"Trending": [
{
"category": "Trending",
"trailerPrice": "",
"isFavourite": null,
"isWatchlist": null
}
]
},
{
"categoryName": "Comedy",
"Comedy": [
{
"category": "Comedy",
"trailerPrice": "",
"isFavourite": null,
"isWatchlist": null
}
]
}
]
""".data(using: .utf8)!

let categories = try! JSONDecoder().decode([Category].self, from: jsonData)

(I changed isFavourit in the JSON to isFavourite since I thought it was a mispelling. It's easy enough to adapt the code if that's not the case)



Related Topics



Leave a reply



Submit