In this article, you’ll learn all about encoding and decoding Dates in Swift to JSON, exploring the basics and advanced topics like custom dates and custom encoding.
Overview
Many programming tasks involve sending data over a network connection, saving data to disk, or submitting data to APIs and services. These tasks often require data to be encoded and decoded to and from an intermediate format while the data is being transferred.
The Swift standard library defines a standardized approach to data encoding and decoding. You adopt this approach by implementing the Encodable and Decodable protocols on your custom types. Adopting these protocols lets implementations of the Encoder and Decoder protocols take your data and encode or decode it to and from an external representation such as JSON or property list. To support both encoding and decoding, declare conformance to Codable, which combines the Encodable and Decodable protocols. This process is known as making your types codable.

While there is a very good tutorial from Apple how to make data types encodable and decodable there is a common problem with dates: There is no JSON standard for dates, much to the distress of every programmer who’s ever worked with them. JSONEncoder and JSONDecoder will by default use a double representation of seconds since January 1st 2001. This format is not very common as a JSON or even WEB format and it is more convenient to store dates in some standard way using days, months, and years.
ISO-8601 Date Encoding
ISO-8601 is the web’s standard way of referring to dates and times, and looks like this: 2021-03-13T19:07:34+00:00. In English this is 13th of march 2021 at 7.34 am. Both JSONEncoder and JSONDecoder are able to use this date format rather than the double default. All you have to do is set their dateEncodingStrategy and dateDecodingStrategy properties.
So assume we have the following JSON data model and want to instance of the given person class.
{
"firstName" : "John",
"lastName" : "Appleseed",
"timestamp" : "2021-03-13T16:43:56+00:00"
}
struct Person: Codable {
let firstName: String
let lastName: String
let timestamp: Date
}
The following code snippet would allow us to decode this into a person struct.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let person = try decoder.decode(Person.self, from: data)
And if we would like to transform our object back into JSON data we could do like this:
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(person)
But how could we handle different date formats in the same JSON string as we can only specify one transformation strategy.
Decoding multiple date formats
Let’s assume we have the same Person JSON as above with an additional birthday property that contains a simple date.
{
"firstname" : "John",
"lastname" : "Appleseed",
"birthdate" : "1973-02-03",
"timestamp" : "2021-11 -19T16:39:57-08:00" // optional
}
The basic idea to handle this challenge is to write a customer decoder using a keyed container. So let’s start defining the date formatters for the used dates within a DateFormatter extension.
extension DateFormatter {
static let simple: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}
For basic types as String, Integers or Floats we have decoding methods in the KeyedDecodingContainerProtocol that is implemented by the container used to decode data. To add the possibility to decode with a special date format we are adding two methods to this protocol. These decoding methods take additional parameters as the format todo the decoding. Let’s define the extension:
extension KeyedDecodingContainerProtocol {
func decode(_ type: Date.Type, forKey key: Self.Key, dateFormatter formatter: DateFormatter) throws -> Date {
let dateString = try decode(String.self, forKey: key)
return try parse(dateString, from: key, with: formatter)
}
func decodeIfPresent(_ type: Date.Type, forKey key: Self.Key, dateFormatter formatter: DateFormatter) throws -> Date? {
guard let dateString = try decodeIfPresent(String.self, forKey: key) else {
return nil
}
return try parse(dateString, from: key, with: formatter)
}
private func parse(_ dateString: String, from key: Self.Key, with formatter: DateFormatter) throws -> Date {
if let date = formatter.date(from: dateString) {
return date
} else {
throw DecodingError.dataCorruptedError(forKey: key,
in: self,
debugDescription: "Wrong Date Format")
}
}
}
With this sharp extension the code to decode dates in json becomes very easy and straight forward.
struct Person: Codable {
let firstName: String
let lastName: String
let birthdate: Date
let timestamp: Date?
enum CodingKeys: String, CodingKey {
case firstName, lastName, birthdate, timestamp
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
birthdate = try container.decode(Date.self, forKey: .birthdate, dateFormatter: .simple)
timestamp = try container.decodeIfPresent(Date.self, forKey: .timestamp, dateFormatter: .ISO8601)
}
}
Encoding multiple date formats
And last but not least we probably need to encode our Person objects into JSONS. We can use the analog mechanism todo this but this time with the KeyedEncodingContainerProtocol KeyedEncodingContainerProtocol.
extension KeyedEncodingContainerProtocol {
public mutating func encode(_ date: Date, forKey key: Self.Key, dateFormatter formatter: DateFormatter) throws {
try encodeIfPresent(date, forKey: key, dateFormatter: formatter)
}
public mutating func encodeIfPresent(_ date: Date?, forKey key: Self.Key, dateFormatter formatter: DateFormatter) throws {
if let d = date {
let dateString: String = formatter.string(from: d)
try encode(dateString, forKey: key)
}
}
}
So lets create an extension of our Person struct so we can decode as well:
extension Person: Codable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstname, forKey: .firstname)
try container.encode(lastname, forKey: .lastname)
try container.encode(birthDate, forKey: .birthDate, dateFormatter: .simple)
try container.encodeIfPresent(updated, forKey: .updated, dateFormatter: .iso8601)
}
}
