2 votes

La stratégie dateEncodingStrategy de JSONEncoder ne fonctionne pas

J'essaie de sérialiser une structure vers un String en utilisant Encodable+JSONEncoder de Swift 4. L'objet peut contenir des valeurs hétérogènes comme String, Array, Date, Int etc.

L'approche utilisée fonctionne bien, à l'exception de la date. La méthode de JSONEncoder dateEncodingStrategy n'a pas d'effet.

Voici un extrait qui reproduit le comportement dans 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))

Sortie :

"{"key":"bar1","value":"12345"}\n"
"{"key":"bar2","value":12345}\n"
"{"key":"bar3","value":539682026.06086397}\n"

Pour l'objet bar3 : Je m'attends à quelque chose comme "{"key":"bar3","value":"Thurs, 3 Jan 1991"}\n" mais il renvoie la date dans le format par défaut de la stratégie .deferToDate.

##EDIT 1###

J'ai donc exécuté le même code dans XCode 9 et il donne le résultat attendu, c'est-à-dire qu'il formate correctement la date en chaîne. Je pense que la 9.2 a une mise à jour mineure vers Swift 4 qui casse cette fonctionnalité. Je ne suis pas sûr de ce qu'il faut faire ensuite.

##EDIT 2##

Comme solution temporaire, j'ai utilisé l'extrait suivant avant de passer à l'approche de @Hamish qui utilise une fermeture.

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)
        }

    }
}

8voto

Hamish Points 42073

Lors de l'utilisation d'une stratégie d'encodage de date personnalisée, l'encodeur intercepte les appels pour coder un Date dans un conteneur donné, puis applique la stratégie personnalisée .

Cependant, avec votre EncodableValue vous ne donnez pas à l'encodeur la possibilité de le faire parce que vous faites appel directement à la fonction encode(to:) méthode. Avec Date ce codera la valeur en utilisant sa représentation par défaut qui est comme son timeIntervalSinceReferenceDate .

Pour résoudre ce problème, vous devez encoder la valeur sous-jacente dans un conteneur de valeur unique afin de déclencher toute stratégie d'encodage personnalisée. Le seul obstacle à cette démarche est le fait que les protocoles ne se conforment pas à eux-mêmes Vous ne pouvez donc pas appeler la fonction encode(_:) avec une méthode Encodable (car le paramètre prend un <Value : Encodable> ).

Une solution à ce problème consiste à définir un Encodable pour l'encodage dans un conteneur de valeur unique, que vous pouvez ensuite utiliser dans votre 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)
  }
}

Cela permet de tirer parti du fait que les membres de l'extension du protocole ont une fonction implicite <Self : P>P est le protocole en cours d'extension, et l'implicite self est tapé comme cet espace réservé (pour faire court, cela nous permet d'appeler la fonction encode(_:) avec une méthode Encodable type conforme).

Une autre option est d'avoir un initialisateur générique sur votre wrapper qui efface le type en stockant une fermeture qui fait l'encodage :

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)
  }
}

Dans les deux cas, vous pouvez maintenant utiliser ce wrapper pour encoder des encodables hétérogènes tout en respectant les stratégies d'encodage personnalisées :

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"}

1voto

Rob Points 70987

Vous pouvez éliminer le EncodableValue et utiliser un générique à la place :

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)

Cela donne :

{"key":"date","value":"Wed, 7 Feb 2018"}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X