问题
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 used approach works fine with the exception of Date.
JSONEncoder's dateEncodingStrategy
property is not having any effect.
Here is a snippet which reproduces the behaviour in Playground:
struct EncodableValue:Encodable {
var value: Encodable
init(_ value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
struct Bar: Encodable, CustomStringConvertible {
let key: String?
let value: EncodableValue?
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)
let jsonData = try? encoder.encode(self)
return String(data: jsonData!, encoding: .utf8)!
}
}
let bar1 = Bar(key: "bar1", value: EncodableValue("12345"))
let bar2 = Bar(key: "bar2", value: EncodableValue(12345))
let bar3 = Bar(key: "bar3", value: EncodableValue(Date()))
print(String(describing: bar1))
print(String(describing: bar2))
print(String(describing: bar3))
Output:
"{"key":"bar1","value":"12345"}\n"
"{"key":"bar2","value":12345}\n"
"{"key":"bar3","value":539682026.06086397}\n"
For bar3 object: I'm expecting something like "{"key":"bar3","value":"Thurs, 3 Jan 1991"}\n"
, but it returns the date in the default .deferToDate strategy format.
##EDIT 1##
So I ran the same code in XCode 9 and it gives the expected output, i.e. correctly formats the date to string. I'm thinking 9.2 has a minor upgrade to Swift 4 which is breaking this feature. Not sure what to do next.
##EDIT 2##
As a temp remedy I'd used the following snippet before changing to @Hamish's approach using a closure.
struct EncodableValue:Encodable {
var value: Encodable
init(_ value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
if let date = value as? Date {
var container = encoder.singleValueContainer()
try container.encode(date)
}
else {
try value.encode(to: encoder)
}
}
}
回答1:
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"}
回答2:
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"}
来源:https://stackoverflow.com/questions/48658574/jsonencoders-dateencodingstrategy-not-working