Je remplace mon ancien code d'analyse JSON par Swift's Codable et je rencontre un problème. Je suppose que ce n'est pas autant une question Codable que c'est une question DateFormatter.
Commencez avec une structure
struct JustADate: Codable {
var date: Date
}
et une chaîne json
let json = """
{ "date": "2017-06-19T18:43:19Z" }
"""
laisse maintenant décoder
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let data = json.data(using: .utf8)!
let justADate = try! decoder.decode(JustADate.self, from: data) //all good
Mais si nous changeons la date pour qu’elle ait une fraction de seconde, par exemple:
let json = """
{ "date": "2017-06-19T18:43:19.532Z" }
"""
Maintenant ça casse. Les dates reviennent parfois avec des fractions de secondes et parfois pas. Auparavant, dans mon code de mappage, j'avais une fonction de transformation qui testait les deux formats de date avec et sans les fractions de seconde. Je ne sais pas trop comment l'aborder avec Codable cependant. Aucune suggestion?
Vous pouvez utiliser deux formateurs de date différents (avec et sans fraction de seconde) et créer un DateDecodingStrategy personnalisé. En cas d'échec lors de l'analyse de la date renvoyée par l'API, vous pouvez émettre une DecodingError comme suggéré par @PauloMattos dans les commentaires:
iOS 9, macOS 10.9, tvOS 9, watchOS 2, Xcode 9 ou version ultérieure
La coutume ISO8601 DateFormatter:
extension Formatter {
static let iso8601: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
return formatter
}()
static let iso8601noFS: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
return formatter
}()
}
La coutume DateDecodingStrategy
et Error
:
extension JSONDecoder.DateDecodingStrategy {
static let customISO8601 = custom { decoder throws -> Date in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
if let date = Formatter.iso8601.date(from: string) ?? Formatter.iso8601noFS.date(from: string) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
}
}
La coutume DateEncodingStrategy
:
extension JSONEncoder.DateEncodingStrategy {
static let customISO8601 = custom { date, encoder throws in
var container = encoder.singleValueContainer()
try container.encode(Formatter.iso8601.string(from: date))
}
}
edit/update:
Xcode 9 • Swift 4 • iOS 11 ou version ultérieure
ISO8601DateFormatter
prend désormais en charge formatOptions
.withFractionalSeconds
sous iOS11 ou version ultérieure:
extension Formatter {
static let iso8601: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
static let iso8601noFS = ISO8601DateFormatter()
}
Les coutumes DateDecodingStrategy
et DateEncodingStrategy
seraient les mêmes que celles indiquées ci-dessus.
// Playground testing
struct ISODates: Codable {
let dateWith9FS: Date
let dateWith3FS: Date
let dateWith2FS: Date
let dateWithoutFS: Date
}
let isoDatesJSON = """
{
"dateWith9FS": "2017-06-19T18:43:19.532123456Z",
"dateWith3FS": "2017-06-19T18:43:19.532Z",
"dateWith2FS": "2017-06-19T18:43:19.53Z",
"dateWithoutFS": "2017-06-19T18:43:19Z",
}
"""
let isoDatesData = Data(isoDatesJSON.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .customISO8601
do {
let isoDates = try decoder.decode(ISODates.self, from: isoDatesData)
print(Formatter.iso8601.string(from: isoDates.dateWith9FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601.string(from: isoDates.dateWith3FS)) // 2017-06-19T18:43:19.532Z
print(Formatter.iso8601.string(from: isoDates.dateWith2FS)) // 2017-06-19T18:43:19.530Z
print(Formatter.iso8601.string(from: isoDates.dateWithoutFS)) // 2017-06-19T18:43:19.000Z
} catch {
print(error)
}
À la place de la réponse de @ Leo et si vous devez prendre en charge les anciens systèmes d’exploitation (ISO8601DateFormatter
est disponible uniquement à partir de iOS 10, mac OS 10.12), vous pouvez écrire un formateur personnalisé qui utilise les deux formats lors de l’analyse de la chaîne:
class MyISO8601Formatter: DateFormatter {
static let formatters: [DateFormatter] = [
iso8601Formatter(withFractional: true),
iso8601Formatter(withFractional: false)
]
static func iso8601Formatter(withFractional fractional: Bool) -> DateFormatter {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss\(fractional ? ".SSS" : "")XXXXX"
return formatter
}
override public func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer<AnyObject?>?,
for string: String,
errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>?) -> Bool {
guard let date = (type(of: self).formatters.flatMap { $0.date(from: string) }).first else {
error?.pointee = "Invalid ISO8601 date: \(string)" as NSString
return false
}
obj?.pointee = date as NSDate
return true
}
override public func string(for obj: Any?) -> String? {
guard let date = obj as? Date else { return nil }
return type(of: self).formatters.flatMap { $0.string(from: date) }.first
}
}
, que vous pouvez utiliser comme stratégie de décodage de date:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
Même si sa mise en œuvre est un peu plus laide, cela présente l’avantage d’être cohérent avec les erreurs de décodage générées par Swift en cas de données malformées, car nous ne modifions pas le mécanisme de rapport d’erreurs).
Par exemple:
struct TestDate: Codable {
let date: Date
}
// I don't advocate the forced unwrap, this is for demo purposes only
let jsonString = "{\"date\":\"2017-06-19T18:43:19Z\"}"
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(MyISO8601Formatter())
do {
print(try decoder.decode(TestDate.self, from: jsonData))
} catch {
print("Encountered error while decoding: \(error)")
}
va imprimer TestDate(date: 2017-06-19 18:43:19 +0000)
Ajout de la partie décimale
let jsonString = "{\"date\":\"2017-06-19T18:43:19.123Z\"}"
donnera le même résultat: TestDate(date: 2017-06-19 18:43:19 +0000)
Cependant, en utilisant une chaîne incorrecte:
let jsonString = "{\"date\":\"2017-06-19T18:43:19.123AAA\"}"
affichera l'erreur Swift par défaut en cas de données incorrectes:
Encountered error while decoding: dataCorrupted(Swift.DecodingError.Context(codingPath: [__lldb_expr_84.TestDate.(CodingKeys in _B178608BE4B4E04ECDB8BE2F689B7F4C).date], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))