How do I decode date JSON into Swift model?

Viewed 1741

What kind of data-model do I need to decode this kind of json date format?

Example json:

{
    createdAt: "2021-01-30T22:48:00.469Z",
    updatedAt: "2021-01-30T22:48:00.490Z"
}

I tried using this model but keep getting a decoding error…

struct Date: Decodable {
    var createdAt: Date
    var updatedAt: Date   
}
2 Answers

First, don't name your custom type Date - it conflicts with Date of the standard library. I renamed it to DateInfo:

struct DateInfo: Decodable {
   var createdAt: Date
   var updatedAt: Date   
}

Then, to decode the dates into Date, you need to set the dateDecodingStrategy on the JSONDecoder to choose the date format. In your example, this is a standard iso8601 format, but with fractional seconds, and (thanks to @LeoDabus) which the built-in decoding strategy .iso8601 doesn't support:

So, without fractional seconds, it would have been done like so:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

let dateInfo = try decoder.decode(DateInfo.self, from: jsonData)

But with fractional seconds, it requires some manual work to decode and format using ISO8601DateFormatter. As a matter of convenience, we could create extensions with the custom formatter and the date decoding strategy:

extension Formatter {
   static var customISO8601DateFormatter: ISO8601DateFormatter = {
      let formatter = ISO8601DateFormatter()
      formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
      return formatter
   }()
}

extension JSONDecoder.DateDecodingStrategy {
   static var iso8601WithFractionalSeconds = custom { decoder in
      let dateStr = try decoder.singleValueContainer().decode(String.self)
      let customIsoFormatter = Formatter.customISO8601DateFormatter
      if let date = customIsoFormatter.date(from: dateStr) {
         return date
      }
      throw DecodingError.dataCorrupted(
               DecodingError.Context(codingPath: decoder.codingPath, 
                                     debugDescription: "Invalid date"))
   }
}

and the usage is similar to built-in strategy, except using a custom one:

decoder.dateDecodingStrategy = .iso8601WithFractionalSeconds

this is about the name that you choose, always you have to choose a different name. You can use this code to access and use data:

struct AboutDate: Codable {
    let createdAt, updatedAt: String?
}


extension AboutDate {
    init(data: Data) throws {
        self = try newJSONDecoder().decode(AboutDate.self, from: data)
    }

    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
        guard let data = json.data(using: encoding) else {
            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
        }
        try self.init(data: data)
    }

    init(fromURL url: URL) throws {
        try self.init(data: try Data(contentsOf: url))
    }

    func with(
        createdAt: String?? = nil,
        updatedAt: String?? = nil
    ) -> AboutDate {
        return AboutDate(
            createdAt: createdAt ?? self.createdAt,
            updatedAt: updatedAt ?? self.updatedAt
        )
    }

    func jsonData() throws -> Data {
        return try newJSONEncoder().encode(self)
    }

    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
        return String(data: try self.jsonData(), encoding: encoding)
    }
}

func newJSONDecoder() -> JSONDecoder {
    let decoder = JSONDecoder()
    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
        decoder.dateDecodingStrategy = .iso8601
    }
    return decoder
}

func newJSONEncoder() -> JSONEncoder {
    let encoder = JSONEncoder()
    if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
        encoder.dateEncodingStrategy = .iso8601
    }
    return encoder
}

To use:

do {
   let aboutDate = try AboutDate(json)
}
catch {
   //handle err
}

You can format String to Date, after getting data

Related