Suppose I have the following struct definition and dictionary:
struct Point: Codable {
let x: Int
let y: Int
}
let pointDictionary = [ "x": 0, "y": 1]
// Inital example was too easy so also we might have
struct Score: Codable {
let points: Int
let name: String
}
let scoreDictionary: [String: Any] = [
"points": 10,
"name": "iOSDevZone"
]
Is there a way, without a roundtrip through JSON or PList, to populate an instance of the struct Point
from pointDictionary
?
What I've Tried
I've looked at the Apple docs and can't really find a way.
To be clear, I understand I could write a custom initializer that takes a Dictionary (as I was submitting this question the system matched with an answer that illustrates this), but that's not what I am asking. (And this is not practical in my real situation, this is purely a demonstrative example).
I am asking, given a [String:Any]
Dictionary where the keys match the property names of a struct and the values are convertible to the types of the properties, is there a way of leveraging Decodable
to initialize the struct?
Why a Dictionary init is not desirable Since most responses have been: "Why not implement a dictionary init?"
There are lots of structs and many properties, the dictionaries come from processing bad JSON (that I have no control over).
This is definitely possible because it's how Foundation does it on Darwin:
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}
return value
}
You can pass Any to __JSONDecoder and it'll decode it. Unfortunately, __JSONDecoder is private. But it is also open source, so that's fixable. It's just tedious.
You need to copy roughly 1500 lines of __JSONDecoder implementation and a few supporting types, remove the "private" in front of __JSONDecoder, and then you can add an extension that does what you want:
extension JSONDecoder {
fileprivate var options: _Options {
return _Options(dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy,
userInfo: userInfo)
}
func decode<T : Decodable>(_ type: T.Type, from topLevel: Any) throws -> T {
let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}
return value
}
}
// And then it "just works":
let score = try JSONDecoder().decode(Score.self, from: scoreDictionary)