WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Handling Unexpected Date Type in JSON Decoder

Forums > SwiftUI

Hi all, I'm running into an issue with my JSON decoder and dates (dates really are hard!). When present in the JSON database, they show up in the format of "2011-04-17". My decoder looks like this:

func fetchResults() async {
        do {
            let url = URL(string: "_url_")!
            let (data, _) = try await URLSession.shared.data(from: url)

            let decoder = JSONDecoder()
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy-MM-dd"
            decoder.dateDecodingStrategy = .formatted(formatter)
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            if let decodedResponse = try? decoder.decode(Result.self, from: data) {
                self.result = decodedResponse
            }
        } catch {
            print("Download failed")
        }
    }

And my JSON-side struct looks like this:

    struct Result: Identifiable, Codable, Hashable {
        let id: Int
        let date: Date?
        let name: String?
    }

This all works great when the date appears as above, but crashes and burns when the date that returns is empty, in the form of "".

Xcode provides me with this error description: dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "Date", intValue: nil)], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))

I suspect there is some missing error handling in my decoder for this sort of thing - how can I smartly update my decoder to ignore empty 'date' strings?

2      

@Paul shares a story about a bad date he recently had:

This all works great when the date appears as above, but crashes and burns
when the date that returns is empty, in the form of "". [...snip....]
how can I smartly update my decoder to ignore empty 'date' strings?

Consider reading the Date in as a simple String.

Then in your Result object (ugh! Use a more descriptive name!) you may have a computed var that takes the String form of your date and converts it to a valid Swift Date object.

struct myUsefulObject:  Identifiable, Codable {
        let id: Int
        let date: String // <-- Read in as a string
        let name: String?

        var usefulDateFormat: Date? {   // <-- Make this optional to account for blank dates
        // Use the string format and transform it
         let usefulDate = // ----- transform your string into a valid date object here ---- // 
         return usefulDateFormat
        }

}

See -> More Horrible Date stories

2      

You can also write a custom init to decode the values and an encode(to:) method to encode the values:

//I called this struct Person instead of Result because that's a BAAAAAAD name to use
struct Person: Identifiable, Codable, Hashable {
    let id: Int
    let name: String?
    let date: Date?

    private enum CodingKeys: String, CodingKey {
        case id, name, date
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.name = try? container.decodeIfPresent(String.self, forKey: .name)
        self.date = try? container.decodeIfPresent(Date.self, forKey: .date)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name ?? "", forKey: .name)
        if let date = date {
            //it's a better idea to use the new FormatStyle APIs here, but I couldn't be arsed
            let fmt = DateFormatter()
            fmt.dateFormat = "yyyy-MM-dd"
            try container.encode(fmt.string(from: date), forKey: .date)
        } else {
            try container.encode("", forKey: .date)
        }
    }
}

2      

Brilliant, thanks for the help @Obelix and @roosterboy. Posting my updated code in case others find it useful.

My updated struct based on @Obelix's suggestions (now featuring an improved naming scheme!):

struct Episode: Identifiable, Codable, Hashable
    let id: Int
    let name: String?
    let airDate: String
    ...

    var usefulDateFormat: Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        let usefulDate = formatter.date(from: airDate)
        return usefulDate
    }
}

When called in my real views, I'm structuring it something like this:

if let newDate = episode.usefulDateFormat {
    Text("\(newDate)")
}

I opted to use the "if let" to handle the optional Date? from the struct - this way the view can ignore it if there was an empty string rather than reporting out a default Date.now, or something similar.

@roosterboy, you're correct in highlighting a need for a more applicable date formatting strategy...my solution is US-focused and @twostraws would likely recommend I leverage Apple's FormatStyle tool to decode in a way that's more globally applicable. Work to go!

2      

Hello everyone, this is my first question in here so if anything is off, please let me know.

I'm trying to handle a similar situation like @prburns. But my questions is more about performance.

  1. If we need to compare computed var solution and custom init solution, which would be a wiser selection for high number of operations?
  2. What if I can't change the data type from Date to String and try to go with @roosterboy's solution. And seperate the formatting logic from encoding process. Because I want encoding to be more performant. And make date formattion just when it needed. How can I achieve this? Any ideas would be appreciated.

Thanks in advance.

2      

Decode as String, then Convert to Date:

Change the date property in your Result struct to a String?:

struct Result: Identifiable, Codable, Hashable {
    let id: Int
    let dateString: String? // Decode as String initially
    let name: String?

    var date: Date? { // Computed property to convert to Date
        dateString?.isEmpty == false ? formatter.date(from: dateString!) : nil
    }
}

Remove the dateDecodingStrategy from the decoder.

Access the date property through the computed property:

if let decodedResponse = try? decoder.decode(Result.self, from: data) {
    self.result = decodedResponse
    print(self.result.date) // Access the computed date property
}

. Custom Decoding Logic:

Create a custom initializer for Result:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    id = try container.decode(Int.self, forKey: .id)
    name = try container.decodeIfPresent(String.self, forKey: .name)

    let dateString = try container.decode(String.self, forKey: .date)
    date = dateString.isEmpty ? nil : formatter.date(from: dateString)
}

2      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.