【问题标题】:Swift's JSONDecoder with multiple date formats in a JSON string?Swift 的 JSONDecoder 在 JSON 字符串中有多种日期格式?
【发布时间】:2025-12-13 12:35:02
【问题描述】:

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

但是,我目前正在使用一个返回日期字符串 (yyyy-MM-dd) 和日期时间字符串 (yyyy-MM-dd HH:mm:ss) 的 API,具体取决于属性。有没有办法让JSONDecoder 处理这个问题,因为提供的DateFormatter 对象一次只能处理一个dateFormat

一个笨拙的解决方案是重写随附的Decodable 模型以仅接受字符串作为它们的属性并提供公共Date getter/setter 变量,但这对我来说似乎是一个糟糕的解决方案。有什么想法吗?

【问题讨论】:

标签: swift json-deserialization codable


【解决方案1】:

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)

【讨论】:

    【解决方案2】:

    斯威夫特 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)")
                }
            }
        }
    }
    

    添加一个只能设置的变量有点笨拙,但您可以轻松地将var dateDecodingStrategyFormatters 转换为func setDateDecodingStrategyFormatters(_ formatters: [DateFormatter]? )

    用法

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

    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 有点笨拙,我不推荐它,您应该定义一个函数。但是,这样做是个人喜好。

    【讨论】:

      【解决方案3】:

      试试这个。 (斯威夫特 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
      }
      

      【讨论】:

      【解决方案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
      }
      

      实例化

      user.dob = MyCustomDate(date)
      

      【讨论】:

        【解决方案5】:

        如果您在单个模型中有多个不同格式的日期,则为每个日期应用.dateDecodingStrategy 有点困难。

        点击此处https://gist.github.com/romanroibu/089ec641757604bf78a390654c437cb0 获取方便的解决方案

        【讨论】:

          【解决方案6】:

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

          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)
          

          【讨论】:

            【解决方案7】:

            请尝试配置与此类似的解码器:

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

            【讨论】:

              【解决方案8】:

              有几种方法可以解决这个问题:

              • 您可以创建一个DateFormatter 子类,它首先尝试日期时间字符串格式,如果失败,则尝试纯日期格式
              • 您可以提供.custom Date 解码策略,其中您向Decoder 询问singleValueContainer(),解码一个字符串,并在传递解析日期之前将其传递给您想要的任何格式化程序
              • 您可以围绕Date 类型创建一个包装器,它提供了一个自定义的init(from:)encode(to:) 来执行此操作(但这并不比.custom 策略更好)
              • 您可以按照您的建议使用纯字符串
              • 您可以为使用这些日期的所有类型提供自定义init(from:),并在其中尝试不同的事情

              总而言之,前两种方法可能是最简单和最干净的——您将在不牺牲类型安全的情况下在任何地方保留Codable 的默认综合实现。

              【讨论】:

              • 第一种方法是我正在寻找的方法。谢谢!
              • Codable 似乎很奇怪,所有其他 json 映射信息都是直接从相应的对象提供的(例如,通过 CodingKeys 映射到 json 键),但日期格式是通过 @ 配置的987654334@ 表示整个 DTO 树。过去使用过 Mantle,您提出的最后一个解决方案感觉是最合适的解决方案,尽管这意味着要为其他可能自动生成的字段重复大量映射代码。
              • 我使用了第二种方法.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 }
              【解决方案9】:

              使用单个编码器无法做到这一点。最好的办法是自定义 encode(to encoder:)init(from decoder:) 方法,并为其中一个值提供您自己的翻译,而为另一个值保留内置的日期策略。

              为此考虑将一个或多个格式化程序传递给userInfo 对象可能是值得的。

              【讨论】:

                最近更新 更多