'How to do custom transformations using Codable?
Let's say I need to transform a date string I received from a web service to a Date object.
Using ObjectMapper, that was easy:
class Example: Mappable {
var date: Date?
required init?(map: Map) { }
func mapping(map: Map) {
date <- (map["date_of_interest"], GenericTransform().dateTransform)
}
}
I just had to implement a tranformer ("GenericTransform" in this case) for date, and pass it as an argument along with the key name to decode.
Now, using Codable:
class Example2: Codable {
var name: String
var age: Int
var date: Date?
enum CodingKeys: String, CodingKey {
case name, age
case date = "date_of_interest"
}
}
To transform a date, in my understanding, I'd have to either:
1) Pass a dateDecodingStrategy to my JSONDecoder, which I don't want to, because I'm trying to keep that part of the code as a generic function.
or
2) Implement an init(from decoder: Decoder) inside Example2, which I also don't want to, because of the boilerplate code I'd have to write to decode all the other properties (which would be automatically generated otherwise):
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
let dateString = try container.decode(String.self, forKey: .date)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
if let date = formatter.date(from: dateString) {
self.date = date
} else {
//throw error
}
}
My question is: is there an easier way to do it than options 1 and 2 above? Maybe tweaking the CodingKeys enum somehow?
EDIT:
The problem is not only about dates, actually. In this project that I'm working on, there are many custom transformations being done using TransformOf<ObjectType, JSONType> from ObjectMapper.
For example, a color transformation of a hex code received from a web service into a UIColor is done using this bit of code:
let colorTransform = TransformOf<UIColor, String>(fromJSON: { (value) -> UIColor? in
if let value = value {
return UIColor().hexStringToUIColor(hex: value)
}
return nil
}, toJSON: { _ in
return nil
})
I'm trying to remove ObjectMapper from the project, making these same transformations using Codable, so only using a custom dateDecodingStrategy will not suffice.
How would you guys do it? Implement a custom init(from decoder: Decoder) for every class that has to decode, for example, a color hex code?
Solution 1:[1]
Using dateDecodingStrategy in your case (as you only reference a single date format) is very simpleā¦
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
Solution 2:[2]
We can use custom method for example like decodeAll here. Try in playground.
struct Model: Codable {
var age: Int?
var name: String?
enum CodingKeys: String, CodingKey {
case name
case age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeAll(String.self, forKey: .name)
age = try container.decodeAll(Int.self, forKey: .age)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try? container.encode(name, forKey: .name)
try? container.encode(age, forKey: .age)
}
}
extension KeyedDecodingContainer where K: CodingKey, K: CustomDebugStringConvertible {
func decodeAll<T: Decodable>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T {
if let obj = try? decode(T.self, forKey: key) {
return obj
} else {
if type == String.self {
if let obj = try? decode(Int.self, forKey: key), let val = String(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = String(obj) as? T {
return val
}
} else if type == Int.self {
if let obj = try? decode(String.self, forKey: key), let val = Int(obj) as? T {
return val
} else if let obj = try? decode(Double.self, forKey: key), let val = Int(obj) as? T {
return val
}
} else if type == Double.self {
if let obj = try? decode(String.self, forKey: key), let val = Double(obj) as? T {
return val
} else if let obj = try? decode(Int.self, forKey: key), let val = Double(obj) as? T {
return val
}
}
}
throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Wrong type for: \(key.stringValue)"))
}
}
let json = ##"{ "age": "5", "name": 98 }"##
do {
let obj = try JSONDecoder().decode(Model.self, from: json.data(using: .utf8)!)
print(obj)
} catch {
print(error)
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Ashley Mills |
| Solution 2 |
