Decode ISO 8601 API Dates with JSONDecoder in Swift
Set JSONDecoder.dateDecodingStrategy to .iso8601 so Swift decodes API date strings directly into Date values.
23 Jan 2026, 13:26 UTC

The problem: API dates arrive as strings, not Date values
When a server returns JSON, timestamps usually arrive as text—for example 2026-11-12T06:22:30Z. If your Swift model declares that field as String, you still have to parse it later. If you write a custom init(from:) for every model, you repeat the same date-parsing logic across the codebase.
Takeaway: set dateDecodingStrategy on JSONDecoder to .iso8601, declare the field as Date in a Codable struct, and let Foundation convert the string during decode(_:from:).
How .iso8601 decoding works
JSONDecoder is Foundation's type for decoding JSON into Codable types. Codable combines Encodable and Decodable, letting Swift synthesize encoding and decoding code for your structs. dateDecodingStrategy tells the decoder how to turn JSON date values into Date, which represents a point in time as seconds since the reference date of January 1, 2001.
The .iso8601 strategy parses RFC 3339/ISO 8601 strings such as 2026-11-12T06:22:30Z. RFC 3339 is a profile of ISO 8601 commonly used in HTTP APIs and includes a time zone designator, usually Z for UTC.
Worked configuration
Run this in an Xcode playground or a Swift target that imports Foundation. No special permissions are required.
import Foundation
struct ServerEvent: Codable {
let identifier: Int
let startsAt: Date
let createdAt: Date
}
let json = """
{
\"identifier\": 42,
\"startsAt\": \"2026-11-12T06:22:30Z\",
\"createdAt\": \"2026-11-12T06:00:00Z\"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
do {
let event = try decoder.decode(ServerEvent.self, from: json)
print(event.startsAt)
print(event.startsAt.timeIntervalSince1970)
} catch {
print(\"Decoding failed: \\(error)\")
}
Expected checks: event.startsAt should be a Date corresponding to 06:22:30 UTC on November 12, 2026. Compare timeIntervalSince1970 against an independently computed epoch value to confirm the time zone was applied correctly. If the JSON contains an unparseable string, the decoder throws DecodingError.dataCorrupted and execution jumps to the catch block.
Risks: a malformed date string causes the entire decode call to throw, not just that one property. The strategy also applies to every Date property decoded by that JSONDecoder instance.
Limits and common mistakes
- Not every ISO 8601 variant is accepted. Foundation's
.iso8601strategy may reject week dates, omitted colons in offsets, or local times without a time zone, depending on the operating system release. Invalid strings throwDecodingError.dataCorrupted. - One strategy per decoder instance. If a payload mixes
2026-11-12T06:22:30ZwithNovember 12, 2026, a single.iso8601decoder cannot handle both. Use.customwith a closure that tries several formatters, or use separate decoder instances. - Parsing behavior varies by OS release. Test on the oldest deployment target you support, not just the latest SDK.
- Do not decode dates as
Stringand convert later. Handling parsing at the decoder boundary keeps invalid data out of your model and avoids scattered conversion code.
Strategy comparison
| Strategy | Best for | Trade-off |
|---|---|---|
.iso8601 | APIs that send RFC 3339/ISO 8601 strings | Strict; rejects some ISO variants and OS-dependent formats |
.formatted(DateFormatter) | Fixed non-ISO string layouts | Requires configuring a DateFormatter; locale-sensitive unless set |
.custom | Mixed or legacy date formats | More code; you manage fallback parsing and errors |
.secondsSince1970 | Unix timestamps as numbers | Only works for numeric timestamps, not strings |
Diagnostic decision: the decoder throws
If decoding fails, inspect the associated value from DecodingError. A dataCorrupted error with a context describing the date key usually means the string did not match the expected format. Log the raw JSON value for that key before deciding whether to relax the strategy or switch to .custom.
Practical verification
- Decode a known timestamp and print
date.timeIntervalSince1970. - Compare that value to an epoch calculator for the same UTC instant.
- Replace the valid string with a malformed one, such as
2026-13-99T99:99:99Z, and confirmdecode(_:from:)throws. - Repeat on the oldest supported deployment target because ISO 8601 parsing can differ across Foundation releases.
Bottom line
Use decoder.dateDecodingStrategy = .iso8601 when your API sends RFC 3339 date strings and you want Date properties populated automatically. Keep one decoder per format, test malformed input, and verify on your minimum deployment target.