Skip to main content

Parsing ISO 8601 dates with milliseconds using Codable

At my job, I’ve written many apps that connect to JSON API’s. Some of those APIs use Node.js for their backend. Node encodes dates in JSON as ISO 8601 formatted strings. But there’s a catch if you want to use this with Codable in Swift.

Using Node, you can simply encode some object to JSON as follows:

const blogPost = {
    "title": "Hello world",
    "date": new Date(),
};

const json = JSON.stringify(blogPost);
console.log(json);
// Prints out: '{"title":"Hello world","date":"2022-08-03T15:51:14.794Z"}'

Now, we can try parsing this using Swift and Codable as follows:

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

struct BlogPost: Codable {
    let title: String
    let date: Date
}

let json = """
{
    "title": "Hello world",
    "date": "2022-08-03T15:51:14.794Z"
}
"""

do {
    let post = try decoder.decode(BlogPost.self, from: json.data(using: .utf8)!)
    print("\(post.date)")
} catch {
    // Prints out: 'The data couldn’t be read because it isn’t in the correct format.'
    print(error.localizedDescription)
}

Oh no! It turns out that this doesn’t work. It prints out an error: The data couldn’t be read because it isn’t in the correct format.

When looking more closely, the error occurred because Codable refuses to parse the date. The culprit are the milliseconds in the date! When we remove them, it decodes just fine.

Why does this happen? As it turns out, the ISO 8601 spec for dates does not specify a single format. It actually supports multiple different date formats, with their own options. And Apple doesn’t permit the milliseconds in the default ISO 8601 date parser. It’s an option that you have to explicitly enable.

To do that, we need some boilerplate code:

// Set up an encoder and decoder using custom date decoding and encoding strategies

let iso8601Formatter = ISO8601DateFormatter()
iso8601Formatter.formatOptions = [
    // Use the format used for internet date times, according to the RFC 3339 standard (according to Apple docs)
    .withInternetDateTime,
    // Enable milliseconds
    .withFractionalSeconds
]

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ decoder in
    let container = try decoder.singleValueContainer()
    let dateString = try container.decode(String.self)
    if let date = iso8601Formatter.date(from: dateString) {
        return date
    } else {
        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unable to parse ISO 8601 date because it was in the wrong format")
    }
})

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .custom({ date, encoder in
    let dateString = iso8601Formatter.string(from: date)
    var container = encoder.singleValueContainer()
    try container.encode(dateString)
})

When we combine the above decoder into the previous sample code, it decodes the JSON just fine! It’s a shame that this doesn’t work out of the box. But now you know how to fix it, and hopefully I’ve saved you some time.