I am trying to serialize a struct to a String using Swift 4\'s Encodable+JSONEncoder. The object can hold heterogenous values like String, Array, Date, Int etc.
The use
You can eliminate the EncodableValue
wrapper, and use a generic instead:
struct Bar<T: Encodable>: Encodable {
let key: String
let value: T?
var json: String {
let encoder = JSONEncoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E, d MMM yyyy"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let data = try! encoder.encode(self)
return String(data: data, encoding: .utf8)!
}
}
let bar = Bar(key: "date", value: Date())
print(bar.json)
That yields:
{"key":"date","value":"Wed, 7 Feb 2018"}
When using a custom date encoding strategy, the encoder intercepts calls to encode a Date
in a given container and then applies the custom strategy.
However with your EncodableValue
wrapper, you're not giving the encoder the chance to do this because you're calling directly into the underlying value's encode(to:)
method. With Date
, this will encode the value using its default representation, which is as its timeIntervalSinceReferenceDate.
To fix this, you need to encode the underlying value in a single value container to trigger any custom encoding strategies. The only obstacle to doing this is the fact that protocols don't conform to themselves, so you cannot call a container's encode(_:)
method with an Encodable
argument (as the parameter takes a <Value : Encodable>
).
One solution to this problem is to define an Encodable
extension for encoding into a single value container, which you can then use in your wrapper:
extension Encodable {
fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
try container.encode(self)
}
}
struct AnyEncodable : Encodable {
var value: Encodable
init(_ value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try value.encode(to: &container)
}
}
This takes advantage of the fact that protocol extension members have an implicit <Self : P>
placeholder where P
is the protocol being extended, and the implicit self
argument is typed as this placeholder (long story short; it allows us to call the encode(_:)
method with an Encodable
conforming type).
Another option is to have have a generic initialiser on your wrapper that type erases by storing a closure that does the encoding:
struct AnyEncodable : Encodable {
private let _encodeTo: (Encoder) throws -> Void
init<Value : Encodable>(_ value: Value) {
self._encodeTo = { encoder in
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
func encode(to encoder: Encoder) throws {
try _encodeTo(encoder)
}
}
In both cases, you can now use this wrapper to encode heterogenous encodables while respecting custom encoding strategies:
import Foundation
struct Bar : Encodable, CustomStringConvertible {
let key: String
let value: AnyEncodable
var description: String {
let encoder = JSONEncoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E, d MMM yyyy"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
encoder.dateEncodingStrategy = .formatted(dateFormatter)
guard let jsonData = try? encoder.encode(self) else {
return "Bar(key: \(key as Any), value: \(value as Any))"
}
return String(decoding: jsonData, as: UTF8.self)
}
}
print(Bar(key: "bar1", value: AnyEncodable("12345")))
// {"key":"bar1","value":"12345"}
print(Bar(key: "bar2", value: AnyEncodable(12345)))
// {"key":"bar2","value":12345}
print(Bar(key: "bar3", value: AnyEncodable(Date())))
// {"key":"bar3","value":"Wed, 7 Feb 2018"}