Swift的JSONDecoder在JSON字符串中有多种日期格式?

Ram*_*att 21 json-deserialization swift codable

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

但是,我目前正在使用一个API,它返回date strings(yyyy-MM-dd)和datetime strings(yyyy-MM-dd HH:mm:ss),具体取决于属性.有没有办法JSONDecoder处理这个,因为提供的DateFormatter对象一次只能处理dateFormat一个?

一个火腿解决方案是重写附带的Decodable模型,只接受字符串作为其属性,并提供公共Dategetter/setter变量,但这对我来说似乎是一个糟糕的解决方案.有什么想法吗?

Les*_*rna 35

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

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
}()
Run Code Online (Sandbox Code Playgroud)


Ita*_*ber 30

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

  • 您可以创建一个DateFormatter首先尝试日期时间字符串格式的子类,然后如果失败,则尝试普通日期格式
  • 您可以提供一种.custom Date解码策略,其中您要求Decodera singleValueContainer(),解码字符串,并在将解析的日期传递出去之前将其传递给您想要的任何格式化程序
  • 您可以创建周围的包装Date提供了一个自定义类型init(from:)encode(to:)它做到这一点(但是这是不是真的比任何一个更好的.custom策略)
  • 您可以按照建议使用纯字符串
  • 您可以init(from:)在使用这些日期的所有类型上提供自定义,并在那里尝试不同的东西

总而言之,前两种方法可能是最简单和最干净的 - 您将保持默认的合成实现,Codable而不会牺牲类型安全性.

  • 使用`Codable`看起来很奇怪所有其他json映射信息都是直接从相应的对象提供的(例如通过`CodingKeys`映射到json键),但日期格式是通过`JSONDecoder`为整个DTO树配置的.在过去使用过Mantle之后,你提出的最后一个解决方案感觉就像是最合适的解决方案,即使它意味着为其他可以自动生成的字段重复大量的映射代码. (2认同)

Oly*_*tre 17

斯威夫特 5

实际上基于使用JSONDecoder扩展的@BrownsooHan 版本

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)")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

用法

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

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
    }()
}
Run Code Online (Sandbox Code Playgroud)

您现在可以通过设置直接将这些分配给解码器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)
}
Run Code Online (Sandbox Code Playgroud)

旁注

再次我知道,设定dateDecodingStrategyFormatters一个var有点哈克,而且我不推荐它,你应该定义一个函数。然而,这样做是个人偏好。

  • 在 swift 5 中进行测试,适用于多种格式。谢谢! (2认同)

S.M*_*ore 14

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

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)")
            }
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

此扩展允许您为JSONDecoder创建DateDecodingStrategy,以处理同一JSON字符串中的多种不同日期格式.该扩展包含一个函数,该函数需要实现一个为您提供CodingKey的闭包,并且您可以为所提供的密钥提供正确的DateFormatter.

假设您有以下JSON:

{
    "publication_date": "2017-11-02",
    "opening_date": "2017-11-03",
    "date_updated": "2017-11-08 17:45:14"
}
Run Code Online (Sandbox Code Playgroud)

以下结构:

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"
    }
}
Run Code Online (Sandbox Code Playgroud)

然后要解码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)
Run Code Online (Sandbox Code Playgroud)


Bro*_*Han 10

尝试这个。(第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
}
Run Code Online (Sandbox Code Playgroud)