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
Read Logs Using the New Swift Os_Log API
Swiftui - Navigationview Title and Back Button Clipped Under Status Bar After Orientation Change
Could Not Build Module 'Nanopb' Error in Xcode 12.0.1
Xcode 8 How to Show Description of Function While Typing
Peek/Pop Preview Ignores Cell Corner Radius in Collection View
Self.Type Cannot Be Directly Converted to Anyclass in Extension to Objective-C Class in Swift
Detect When Uitableviewcell Did Load
Import Xctest into a Dynamic Framework
Swift 4 Uicollectionview Detect End of Scrolling
Alamofireimage Disk Cache Not Working
Setting a Stateobject Value from Child View Causes Navigationview to Pop All Views
Avcapturesession and Avaudiosession Recording Video While Background Music Playing Only Works Once
How to Create a CSV File from Core Data (Swift)
How to Change Button Text Size in iOS 8 Swift
How to Make an API Call When the User Terminates the App
How to Use Nsuserdefaults to Store an Array of Custom Classes in Swift