3

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   
}
marc-medley
  • 8,931
  • 5
  • 60
  • 66
  • You need a custom date decoding strategy and/or a custom date formatter – Leo Dabus Jan 31 '21 at 00:58
  • https://stackoverflow.com/a/28016692/2303865 – Leo Dabus Jan 31 '21 at 00:59
  • You are getting this error because the default dateDecodingStrategy is deferredToDate which expects the number of. seconds since the reference date (January 1st 2001 midnight UTC) – Leo Dabus Jan 31 '21 at 01:01
  • I'm surprised, in one of your [previous questions](https://stackoverflow.com/questions/65961704/swift-json-decoding-nested-array-dictionary-to-flat-model) you wrote *Yes I have a a custom date-decoding strategy not included in this code:* – vadian Jan 31 '21 at 08:55
  • 1
    Vadian, I have been studying coding (Swift) iOS for 3 months now so I still struggle with the basics and figuring out to make code reusable... –  Jan 31 '21 at 13:47

2 Answers2

5

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
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • this doesn't support fractional seconds – Leo Dabus Jan 31 '21 at 15:41
  • @LeoDabus, ah, true.... damn, that's pretty annoying that it doesn't support fractional seconds. Thanks - updated the answer – New Dev Jan 31 '21 at 17:52
  • I have already posted a link with this approach yesterday. Btw there is no need to set the timezone to UTC which is already the default. You can also write `.init` instead of DecodingError.Context (type inferred) and no need to declare customISO8601DateFormatter as a var – Leo Dabus Jan 31 '21 at 17:55
  • You should mention that you copied&pasted the code almost literally from [Leo's answer](https://stackoverflow.com/questions/28016578/how-can-i-parse-create-a-date-time-stamp-formatted-with-fractional-seconds-utc/28016692#28016692) – vadian Jan 31 '21 at 17:56
  • @vadian, hmm.. it does have resemblance, but i didn't copy-paste it. – New Dev Jan 31 '21 at 17:59
  • From the docs **"The time zone used to create and parse date representations. When unspecified, GMT is used."** https://developer.apple.com/documentation/foundation/iso8601dateformatter/1643185-timezone – Leo Dabus Jan 31 '21 at 18:02
  • @LeoDabus, ah.. true. there was an example with setting timeZone in [documentation](https://developer.apple.com/documentation/foundation/dateformatter#2528261), but that's for a DateFormatter. – New Dev Jan 31 '21 at 18:03
  • Ok so dumb question: I added the Formatter and Json.DateDecoding Extensions but where do I type deocder.dateDecodingStrategy = .iso860WithFractionalSeconds? Does it go in the data model's initializer: init(from decoder) throws { }? I have some custom mapping set-up there.... –  Jan 31 '21 at 23:35
  • @Chris, no - you set this property on the JSON decoder at the point where you do the decoding. It's not part of the data model. See my first example. – New Dev Feb 01 '21 at 01:15
-1

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