如何在 JSON string 中使用多种日期格式来解析 JSONDecoder?(Swift 的问题)

48

JSONDecoder提供了dateDecodingStrategy属性,允许我们根据DateFormatter对象定义如何解释传入的日期字符串。

然而,我目前正在使用一个API,该API根据属性返回日期字符串(yyyy-MM-dd)和日期时间字符串(yyyy-MM-dd HH:mm:ss)。由于提供的DateFormatter对象一次只能处理一个dateFormat,因此有没有办法让JSONDecoder处理这个问题呢?

一个笨拙的解决方案是重写相应的Decodable模型,只接受字符串作为它们的属性,并提供公共的Date getter/setter变量,但我认为这是一个不好的解决方案。有什么想法吗?


https://dev59.com/SFYO5IYBdhLWcg3wDNQb - Leo Dabus
我编写了一个简单的扩展程序,用于KeyedDecodingContainer和有效地解析日期。请向下滚动并检查我的答案 https://dev59.com/9lcP5IYBdhLWcg3wP3wU#70304185 - Kishore Kankata
另一种方法是使用DateDecodingStrategy的扩展 https://dev59.com/9lcP5IYBdhLWcg3wP3wU#74017518 - KPM
10个回答

53
请尝试与此类似配置的解码器:
lazy var decoder: JSONDecoder = {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
        let container = try decoder.singleValueContainer()
        let dateStr = try container.decode(String.self)
        // possible date strings: "2016-05-01",  "2016-07-04T17:37:21.119229Z", "2018-05-20T15:00:00Z"
        let len = dateStr.count
        var date: Date? = nil
        if len == 10 {
            date = dateNoTimeFormatter.date(from: dateStr)
        } else if len == 20 {
            date = isoDateFormatter.date(from: dateStr)
        } else {
            date = self.serverFullDateFormatter.date(from: dateStr)
        }
        guard let date_ = date else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateStr)")
        }
        print("DATE DECODER \(dateStr) to \(date_)")
        return date_
    })
    return decoder
}()

看这里一个关于如何设置dateNoTimeFormatter的例子:https://dev59.com/TnkPtIcB2Jgan1znmoYN - Oded Ben Dov

40

有几种处理方法:

  • 您可以创建一个 DateFormatter 子类,首先尝试日期时间字符串格式,然后如果失败,则尝试纯日期格式
  • 您可以提供一个 .customDate 解码策略,在其中请求 Decoder 提供一个 singleValueContainer(),解码一个字符串,并在传递解析出的日期之前通过任何格式化程序传递它
  • 您可以创建一个包装器,围绕 Date 类型提供自定义的 init(from:)encode(to:) 来做这件事(但这并不比 .custom 策略更好)
  • 您可以使用纯字符串,就像您建议的那样
  • 您可以为所有使用这些日期的类型提供自定义的 init(from:),并在其中尝试不同的方法

总的来说,前两种方法可能是最简单和最清晰的-您将在不牺牲类型安全的情况下保留默认合成的 Codable 实现。


第一种方法是我正在寻找的。谢谢! - RamwiseMatt
3
使用Codable时,似乎有些奇怪,所有其他的JSON映射信息都是直接从相应的对象中提供的(例如通过CodingKeys将映射到JSON键),但日期格式设置是在整个DTO树上通过JSONDecoder进行配置的。过去使用过Mantle,在你提出的解决方案中,最后一个感觉是最合适的,即使这意味着需要重复很多本来可以自动生成的其他字段的映射代码。 - fabb
我使用了第二种方法.dateDecodingStrategy = .custom { decoder in var container = try decoder.singleValueContainer(); let text = try container.decode(String.self); guard let date = serverDateFormatter1.date(from: text) ?? serverDateFormatter2.date(from: text) else { throw BadDate(text) }; return date } - Daniel T.

25

Swift 5

实际上,基于@BrownsooHan版本使用了JSONDecoder扩展。

JSONDecoder+dateDecodingStrategyFormatters.swift

extension JSONDecoder {

    /// Assign multiple DateFormatter to dateDecodingStrategy
    ///
    /// Usage :
    ///
    ///      decoder.dateDecodingStrategyFormatters = [ DateFormatter.standard, DateFormatter.yearMonthDay ]
    ///
    /// The decoder will now be able to decode two DateFormat, the 'standard' one and the 'yearMonthDay'
    ///
    /// Throws a 'DecodingError.dataCorruptedError' if an unsupported date format is found while parsing the document
    var dateDecodingStrategyFormatters: [DateFormatter]? {
        @available(*, unavailable, message: "This variable is meant to be set only")
        get { return nil }
        set {
            guard let formatters = newValue else { return }
            self.dateDecodingStrategy = .custom { decoder in

                let container = try decoder.singleValueContainer()
                let dateString = try container.decode(String.self)

                for formatter in formatters {
                    if let date = formatter.date(from: dateString) {
                        return date
                    }
                }

                throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
            }
        }
    }
}

这是一种有点hacky的方式,可以添加一个只能设置的变量,但您可以通过 func setDateDecodingStrategyFormatters(_ formatters: [DateFormatter]? ) 轻松地转换 var dateDecodingStrategyFormatters

用法

假设您已经在代码中定义了几个DateFormatter,如下所示:

extension DateFormatter {
    static let standardT: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        return dateFormatter
    }()

    static let standard: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return dateFormatter
    }()

    static let yearMonthDay: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        return dateFormatter
    }()
}

现在你可以通过设置 dateDecodingStrategyFormatters 直接将它们分配给解码器:

// Data structure
struct Dates: Codable {
    var date1: Date
    var date2: Date
    var date3: Date
}

// The Json to decode 
let jsonData = """
{
    "date1": "2019-05-30 15:18:00",
    "date2": "2019-05-30T05:18:00",
    "date3": "2019-04-17"
}
""".data(using: .utf8)!

// Assigning mutliple DateFormatters
let decoder = JSONDecoder()
decoder.dateDecodingStrategyFormatters = [ DateFormatter.standardT,
                                           DateFormatter.standard,
                                           DateFormatter.yearMonthDay ]


do {
    let dates = try decoder.decode(Dates.self, from: jsonData)
    print(dates)
} catch let err as DecodingError {
    print(err.localizedDescription)
}

注释

我知道将 dateDecodingStrategyFormatters 设置为 var 有些hacky,我不建议这样做,你应该定义一个函数来代替。然而,这是个人喜好而已。


2
在Swift 5中进行测试,支持多种格式,非常好用。谢谢! - alexandru.gaidei

23

试试这个。(Swift 4)

let formatter = DateFormatter()

var decoder: JSONDecoder {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom { decoder in
        let container = try decoder.singleValueContainer()
        let dateString = try container.decode(String.self)

        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        if let date = formatter.date(from: dateString) {
            return date
        }
        formatter.dateFormat = "yyyy-MM-dd"
        if let date = formatter.date(from: dateString) {
            return date
        }
        throw DecodingError.dataCorruptedError(in: container,
            debugDescription: "Cannot decode date string \(dateString)")
    }
    return decoder
}

1
每次调用此属性都会创建一个新的日期格式化程序和新的解码器。 - Leo Dabus
https://dev59.com/SFYO5IYBdhLWcg3wDNQb - Leo Dabus

16

面对同样的问题,我编写了以下扩展程序:

extension JSONDecoder.DateDecodingStrategy {
    static func custom(_ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?) -> JSONDecoder.DateDecodingStrategy {
        return .custom({ (decoder) -> Date in
            guard let codingKey = decoder.codingPath.last else {
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No Coding Path Found"))
            }

            guard let container = try? decoder.singleValueContainer(),
                let text = try? container.decode(String.self) else {
                    throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date text"))
            }

            guard let dateFormatter = try formatterForKey(codingKey) else {
                throw DecodingError.dataCorruptedError(in: container, debugDescription: "No date formatter for date text")
            }

            if let date = dateFormatter.date(from: text) {
                return date
            } else {
                throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(text)")
            }
        })
    }
}

这个扩展允许您为JSONDecoder创建一个DateDecodingStrategy,以处理同一JSON字符串中的多种不同日期格式。该扩展包含一个函数,它需要实现一个闭包来给出CodingKey,您需要为提供的键提供正确的DateFormatter。

假设您有以下JSON:

{
    "publication_date": "2017-11-02",
    "opening_date": "2017-11-03",
    "date_updated": "2017-11-08 17:45:14"
}
以下结构体:
struct ResponseDate: Codable {
    var publicationDate: Date
    var openingDate: Date?
    var dateUpdated: Date

    enum CodingKeys: String, CodingKey {
        case publicationDate = "publication_date"
        case openingDate = "opening_date"
        case dateUpdated = "date_updated"
    }
}

然后要解码JSON,您可以使用以下代码:

let dateFormatterWithTime: DateFormatter = {
    let formatter = DateFormatter()

    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

    return formatter
}()

let dateFormatterWithoutTime: DateFormatter = {
    let formatter = DateFormatter()

    formatter.dateFormat = "yyyy-MM-dd"

    return formatter
}()

let decoder = JSONDecoder()

decoder.dateDecodingStrategy = .custom({ (key) -> DateFormatter? in
    switch key {
    case ResponseDate.CodingKeys.publicationDate, ResponseDate.CodingKeys.openingDate:
        return dateFormatterWithoutTime
    default:
        return dateFormatterWithTime
    }
})

let results = try? decoder.decode(ResponseDate.self, from: data)

4

这种方法可能有点啰嗦,但更加灵活: 用另一个Date类包装日期,并为其实现自定义序列化方法。例如:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"

class MyCustomDate: Codable {
    var date: Date

    required init?(_ date: Date?) {
        if let date = date {
            self.date = date
        } else {
            return nil
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        let string = dateFormatter.string(from: date)
        try container.encode(string)
    }

    required public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let raw = try container.decode(String.self)
        if let date = dateFormatter.date(from: raw) {
            self.date = date
        } else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot parse date")
        }
    }
}

现在,您不再依赖于 .dateDecodingStrategy.dateEncodingStrategy,您的MyCustomDate日期将以指定的格式解析。在类中使用它:

class User: Codable {
    var dob: MyCustomDate
}

使用Instantiate

user.dob = MyCustomDate(date)

3
我已经在DateDecodingStrategy中定义了这个扩展,它接受一个日期格式化程序数组作为参数:
extension JSONDecoder.DateDecodingStrategy {
  static func anyFormatter(in formatters: [DateFormatter]) -> Self {
    return .custom { decoder in
      guard formatters.count > 0 else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No date formatter provided"))
      }
      
      guard let dateString = try? decoder.singleValueContainer().decode(String.self) else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date string"))
      }
      
      let successfullyFormattedDates = formatters.lazy.compactMap { $0.date(from: dateString) }
      guard let date = successfullyFormattedDates.first else {
        throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Date string \"\(dateString)\" does not match any of the expected formats (\(formatters.compactMap(\.dateFormat).joined(separator: " or ")))"))
      }
      
      return date
    }
  }
}

我是这样使用它的:

  let format1 = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
  let format2 = DateFormatter(format: "yyyy-MM-dd'T'HH:mmzzzzzz")
  let decoder = JSONDecoder()
  decoder.dateDecodingStrategy = .anyFormatter(in: [. format1, . format2])

它会依次尝试使用提供的不同格式转换日期(懒惰地进行,因此在第一次成功尝试后停止),如果每个格式都失败,则会抛出错误。

2

使用单个编码器无法完成此操作。您最好的选择是自定义encode(to encoder:)init(from decoder:)方法,并为其中一个值提供自己的翻译,同时保留另一个内置日期策略。

值得一提的是,可以考虑将一个或多个格式化程序传递到userInfo对象中以实现此目的。


0

0
添加一个扩展到KeyedDecodingContainer。
extension KeyedDecodingContainer {
func decodeDate(forKey key: KeyedDecodingContainer<K>.Key, withPossible formats: [DateFormatter]) throws -> Date? {
    
    for format in formats {
        if let date = format.date(from: try self.decode(String.self, forKey: key)) {
            return date
        }
    }
    throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Date string does not match format expected by formatter.")
}

}

并使用'try container.decodeDate(forKey: 'key', withPossible: [.iso8601Full, .yyyyMMdd])'

完整解决方案在此处:

    import Foundation

extension DateFormatter {
    static let iso8601Full: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter
    }()
    
    static let yyyyMMdd: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter
    }()
}

public struct RSSFeed: Codable {
        public let releaseDate: Date?
        public let releaseDateAndTime: Date?
}

extension RSSFeed {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        releaseDate = try container.decodeDate(forKey: .releaseDate, withPossible: [.iso8601Full, .yyyyMMdd])
        releaseDateAndTime = try container.decodeDate(forKey: .releaseDateAndTime, withPossible: [.iso8601Full, .yyyyMMdd])
    }
}

extension KeyedDecodingContainer {
    func decodeDate(forKey key: KeyedDecodingContainer<K>.Key, withPossible formats: [DateFormatter]) throws -> Date? {
        
        for format in formats {
            if let date = format.date(from: try self.decode(String.self, forKey: key)) {
                return date
            }
        }
        throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Date string does not match format expected by formatter.")
    }
}

let json = """
{
"releaseDate":"2017-11-12",
"releaseDateAndTime":"2017-11-16 02:02:55"
}
"""

let data = Data(json.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
let rssFeed = try! decoder.decode(RSSFeed.self, from: data)

let feed = rssFeed
print(feed.releaseDate, feed.releaseDateAndTime)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接