Reputation: 6275
I have a custom object stored in a Struct:
struct Event {
let type: String
let value: String
let time: Int
}
where time is a UnixTime in millis. In the App I have a scrollable text field when I print this event in json format
{
"type" : "rain",
"value" : "0.5",
"time" : 1681663944
}
but for time parameter I would like to convert the UnixTime in a human readable form (i.e. 16/04/2023), so I need to customize haw this parameter is encoded. The problem is that all my objects are conforms to this protocol, so I have the toJsonString()
method for free, based on the power of Codable
.
protocol BaseObject: Codable {
}
extension BaseObject {
func toJsonString() -> String {
let jsonEncoder = JSONEncoder()
do {
let jsonData = try jsonEncoder.encode(self)
let json = String(data: jsonData, encoding: String.Encoding.utf8)
return json ?? ""
} catch {
return ""
}
}
}
So, I don't have a toJsonString()
method for each object separately. Naïve solution should be write a custom toJsonString()
for each object, but I have a lot of struct and if I found a smart way to intercept time
attributes I can override the encode representation of all attributes in the App with this type.
Ideas ?
Thanks.
EDIT
For now I found this solution:
func toJsonString() -> String {
let jsonEncoder = JSONEncoder()
do {
let jsonData = try jsonEncoder.encode(self)
var modData = Data()
if var dict = jsonData.toDictionary() {
dict.keys.filter{ $0.contains("time") }.forEach { key in
if let t = dict[key] as? Int {
dict[key] = t.toStringDate()
}
}
modData = dict.toData()
}
let json = String(data: modData, encoding: String.Encoding.utf8)
return json ?? ""
} catch {
return ""
}
}
Upvotes: 1
Views: 549
Reputation: 63399
You can use a property wrapper to hook in and give custom encoding behaviour for a specific property:
import Foundation
@propertyWrapper
struct PrettyEncodedDate {
static let formatter = {
let df = DateFormatter()
// Customize the date formatter however you'd like
df.dateStyle = .full
return df
}()
let wrappedValue: Date
}
extension PrettyEncodedDate: Decodable {
struct InvalidDateError: Error {
let dateString: String
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
guard let date = Self.formatter.date(from: dateString) else {
throw InvalidDateError(dateString: dateString)
}
self.init(wrappedValue: date)
}
}
extension PrettyEncodedDate: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let formattedDate = Self.formatter.string(from: self.wrappedValue)
try container.encode(formattedDate)
}
}
Here's an example usage:
struct Event: Codable {
let type: String
let value: String
@PrettyEncodedDate var time: Date
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let event = Event(type: "some type", value: "some value", time: Date.now)
let prettyJSON = try encoder.encode(event)
print(String(data: prettyJSON, encoding: .utf8)!)
Which prints:
{
"type" : "some type",
"value" : "some value",
"time" : "Sunday, April 16, 2023"
}
This relies on time
to be a Date
(which it probably should be). If it must stay an Int
(though I'd caution against that), you can tweak the property wrapper to change the current behaviour (String -> Date
, Date -> String
) to add an extra processing step (String -> Date -> Int
, Int -> Date -> String
).
Upvotes: 1