Need to encode into JSON, a struct that has 2 Date instance variables (day and time), however, I need to encode each date instance variable with a different format, ie. for "day":"yyyy-M-d" and "time":"H:m:s".
Have written a custom decoder which works no problems. But not sure how to write the required custom encoder to solve this.
For example I can decode the following JSON string: { "biometrics" : [ {"biometricId":1,"amount":2.1,"source":"Alderaan","day":"2019-1-3","time":"11-3-3","unitId":2}, {"biometricId":10,"amount":3.1,"source":"Endoor","day":"2019-2-4","time":"11-4-4","unitId":20}] }
However, when I encode it, I can only encode it in a single date format :( Help, would be greatly appreciated. Thank you.
import UIKit
let biometricsJson = """
{ "biometrics" : [
{"biometricId":1,"amount":2.1,"source":"Alderaan","day":"2019-1-3","time":"11-3-3","unitId":2},
{"biometricId":10,"amount":3.1,"source":"Endoor","day":"2019-2-4","time":"11-4-4","unitId":20}]
}
"""
struct Biometrics: Codable {
var biometrics: [Biometric]
}
struct Biometric: Codable {
var biometricId: Int
var unitId: Int
var source: String?
var amount: Double
var day: Date
var time: Date
init(biometricId: Int, unitId: Int, source: String, amount: Double, day: Date, time: Date){
self.biometricId = biometricId
self.unitId = unitId
self.source = source
self.amount = amount
self.day = day
self.time = time
}
}
extension Biometric {
static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "H:m:s"
if let date = formatter.date(from: dateString) {
return date
}
formatter.dateFormat = "yyyy-M-d"
if let date = formatter.date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Cannot decode date string \(dateString)")
}
return decoder
}()
}
let biometrics = try Biometric.decoder.decode(Biometrics.self, from:biometricsJson.data(using: .utf8)!)
let jsonEncoder = JSONEncoder()
let encodedJson = try jsonEncoder.encode(biometrics)
let jsonString = String(data: encodedJson, encoding: .utf8)
if biometricsJson != jsonString {
print("error: decoding, then encoding does not give the same string")
print("biometricsJson: \(biometricsJson)")
print("jsonString: \(jsonString!)")
}
I expect the encoded JSON, to be decodable by the decoder. i.e. biometricsJson == jsonString
In a custom encode(to:)
, just encode each one as a string using the desired formatter. There's no "date" type in JSON; it's just a string. Something along these lines:
enum CodingKeys: CodingKey {
case biometricId, amount, source, day, time, unitId
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(biometricId, forKey: .biometricId)
try container.encode(unitId, forKey: .unitId)
try container.encode(source, forKey: .source)
try container.encode(amount, forKey: .amount)
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "H:m:s"
let timeString = formatter.string(from: time)
try container.encode(timeString, forKey: .time)
formatter.dateFormat = "yyyy-M-d"
let dayString = formatter.string(from: day)
try container.encode(dayString, forKey: .day)
}
But note that you can't test for equivalent strings. JSON dictionaries aren't order-preserving, so there's no way to guarantee a character-by-character match.
Note that if you really want to have days and times, you should consider DateComponents
rather than a Date
. A date is a specific instance in time; it's not in any time zone, and it can't be just an hour, minute, and second.
Also, your use of Double is going to cause rounding differences. So 2.1 will be encoded as 2.1000000000000001. If that's a problem, you should use Decimal
for amount
rather than Double
.