JSONEncoder's dateEncodingStrategy not working

后端 未结 2 1451
清歌不尽
清歌不尽 2021-01-22 04:45

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

相关标签:
2条回答
  • 2021-01-22 05:28

    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"}
    
    0 讨论(0)
  • 2021-01-22 05:30

    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"}
    
    0 讨论(0)
提交回复
热议问题