Swift's JSONDecoder with multiple date formats in a JSON string?

前端 未结 8 1554
既然无缘
既然无缘 2020-12-14 06:48

Swift\'s JSONDecoder offers a dateDecodingStrategy property, which allows us to define how to interpret incoming date strings in accordance with a

相关标签:
8条回答
  • 2020-12-14 07:21

    There are a few ways to deal with this:

    • You can create a DateFormatter subclass which first attempts the date-time string format, then if it fails, attempts the plain date format
    • You can give a .custom Date decoding strategy wherein you ask the Decoder for a singleValueContainer(), decode a string, and pass it through whatever formatters you want before passing the parsed date out
    • You can create a wrapper around the Date type which provides a custom init(from:) and encode(to:) which does this (but this isn't really any better than a .custom strategy)
    • You can use plain strings, as you suggest
    • You can provide a custom init(from:) on all types which use these dates and attempt different things in there

    All in all, the first two methods are likely going to be the easiest and cleanest — you'll keep the default synthesized implementation of Codable everywhere without sacrificing type safety.

    0 讨论(0)
  • 2020-12-14 07:24

    Facing this same issue, I wrote the following extension:

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

    This extension allows you to create a DateDecodingStrategy for the JSONDecoder that handles multiple different date formats within the same JSON String. The extension contains a function that requires the implementation of a closure that gives you a CodingKey, and it is up to you to provide the correct DateFormatter for the provided key.

    Lets say that you have the following JSON:

    {
        "publication_date": "2017-11-02",
        "opening_date": "2017-11-03",
        "date_updated": "2017-11-08 17:45:14"
    }
    

    The following Struct:

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

    Then to decode the JSON, you would use the following code:

    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)
    
    0 讨论(0)
  • 2020-12-14 07:26

    It is a little verbose, but more flexible approach: wrap date with another Date class, and implement custom serialize methods for it. For example:

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

    So now you are independent of .dateDecodingStrategy and .dateEncodingStrategy and your MyCustomDate dates will parsed with specified format. Use it in class:

    class User: Codable {
        var dob: MyCustomDate
    }
    

    Instantiate with

    user.dob = MyCustomDate(date)
    
    0 讨论(0)
  • 2020-12-14 07:28

    Please try decoder configurated similarly to this:

    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
    }()
    
    0 讨论(0)
  • 2020-12-14 07:36

    If you have multiple dates with different formats in single model, its bit difficult to apply .dateDecodingStrategy for each dates.

    Check here https://gist.github.com/romanroibu/089ec641757604bf78a390654c437cb0 for a handy solution

    0 讨论(0)
  • 2020-12-14 07:39

    try this. (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
    }
    
    0 讨论(0)
提交回复
热议问题