3

I have a JSON (from a third party) that I need to parse. This JSON returns several nested objects

articles: {
  authors: {
    birthday: 'DD-MM-YYYY'
  }
  relevant_until: 'YYYY-MM-DD HH:MM:SS'
  publication_date: secondsSince1970,
  last_comment: iso8601
}

I'm following this answer to have multiple date formatters and it works, as long as every date extracted from JSON is a string.

But when it comes to the secondsSince1970 (UNIX epoc time) I can't find a way to parse it as a codable object. Everywhere I see the Date(timeIntervalSince1970: timestamp) and I don't know how to use it when decoding it

How do I parse the dates on this object when a date can be passed as a TimeInterval or as a String?

try jsonDecoder.decode(Articles.self, from: jsonData)
Nicos Karalis
  • 3,724
  • 4
  • 33
  • 62
  • Have you tried implementing custom `init(from:)` and parse it manually? As a reference https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types#2904058. – krlbsk Nov 18 '20 at 12:31

2 Answers2

5
extension Formatter {
    static let iso8601withFractionalSeconds: DateFormatter = {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
        return formatter
    }()
    static let iso8601: DateFormatter = {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return formatter
    }()
    static let ddMMyyyy: DateFormatter = {
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "dd-MM-yyyy"
        return formatter
    }()
}

extension JSONDecoder.DateDecodingStrategy {
    static let multiple = custom {
        let container = try $0.singleValueContainer()
        do {
            return try Date(timeIntervalSince1970: container.decode(Double.self))
        } catch DecodingError.typeMismatch {
            let string = try container.decode(String.self)
            if let date = Formatter.iso8601withFractionalSeconds.date(from: string) ??
                Formatter.iso8601.date(from: string) ??
                Formatter.ddMMyyyy.date(from: string) {
                return date
            }
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
        }
    }
}

Playground testing:

struct Root: Codable {
    let articles: Articles
}
struct Articles: Codable {
    let authors: Authors
    let relevantUntil: Date
    let publicationDate: Date
    let lastComment: Date
}

struct Authors: Codable {
    let birthday: Date
}

let json = """
{"articles": {
              "authors": {"birthday": "01-01-1970"},
              "relevant_until": "2020-11-19 01:23:45",
              "publication_date": 1605705003.0019,
              "last_comment": "2020-11-19 01:23:45.678"}
}
"""

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .multiple
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
    let root = try decoder.decode(Root.self, from: .init(json.utf8))
    print(root.articles)  // Articles(authors: __lldb_expr_107.Authors(birthday: 1970-01-01 03:00:00 +0000), relevantUntil: 2020-11-19 04:23:45 +0000, publicationDate: 2020-11-18 13:10:03 +0000, lastComment: 2020-11-19 04:23:45 +0000)

} catch {
    print(error)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
2

Following the same logic you can try to decode the JSON property as TimeInterval (or Double) and if that fails, fall back to your String handling:

extension JSONDecoder {
    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()
                do {
                    let timeInterval = try container.decode(TimeInterval.self)
                    return Date(timeIntervalSince1970: timeInterval)
                } catch DecodingError.typeMismatch {
                    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")
            }
        }
    }
}
gcharita
  • 7,729
  • 3
  • 20
  • 37