JSONEncoder's dateEncodingStrategy not working

随声附和 提交于 2019-12-20 02:59:08

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!