Consider the following type:
public struct DocumentDate: Codable {
/// Year component of the date, Integer with no limitation on the range.
public let year: Int
/// Month component of the date, Integer from 1 to 12.
public let month: Int
/// Day component of the date, Integer from 1 to 31.
public let day: Int
}
What would be the easiest way to incorporate Codable
and more specifically Decodable
protocol support into this type to decode an ISO 8601-compliant date of type:
"2012-01-13"
Ideally, I'd like to get a String
decoded in the initializer and then I'll be able to parse it as I wish. What would be the best strategy to implement this support?
I know I can use dateDecodingStrategy but that is to decode a String
into Date
. My type is actually not a Date
, but a custom one, so that's where the challenge is.
My suggestion is to implement init(from:)
and parse the date string with Regular Expression
public struct DocumentDate: Decodable {
/// Year component of the date, Integer with no limitation on the range.
public let year: Int
/// Month component of the date, Integer from 1 to 12.
public let month: Int
/// Day component of the date, Integer from 1 to 31.
public let day: Int
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
guard let match = dateString.wholeMatch(of: /(\d{4})-(\d{2})-(\d{2})/)?.output else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "\(dateString) is not a valid date format")
}
year = Int(match.1)!
month = Int(match.2)!
day = Int(match.3)!
}
}
and an example how to use it
let jsonString = """
{"date":"2012-01-13"}
"""
struct Test: Decodable {
let date: DocumentDate
}
do {
let result = try JSONDecoder().decode(Test.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}
You can even check if month and day are in the valid range for example
let month = Int(match.2)!
guard (1...12).contains(month) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Month value \(month) is out of range") }
self.month = month
An alternative is to create a date with ISO8601DateFormatter
and extract the desired date components
public struct DocumentDate: Decodable {
/// Year component of the date, Integer with no limitation on the range.
public let year: Int
/// Month component of the date, Integer from 1 to 12.
public let month: Int
/// Day component of the date, Integer from 1 to 31.
public let day: Int
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withFullDate
guard let date = formatter.date(from: dateString) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "\(dateString) is not a valid date format")
}
let components = Calendar(identifier: .iso8601).dateComponents([.year, .month, .day], from: date)
year = components.year!
month = components.month!
day = components.day!
}
}