Configure JSONDecoder for snake_case keys and ISO 8601 dates with fractional seconds
Learn how to set up a single JSONDecoder that converts snake_case keys to camelCase properties and parses ISO 8601 timestamps with fractional seconds, plus the pitfalls to watch for.
28 Oct 2025, 13:08 UTC

Useful answer
Create a single JSONDecoder instance, set keyDecodingStrategy to .convertFromSnakeCase, and provide a custom dateDecodingStrategy closure that uses an ISO8601DateFormatter with fractional‑second support. Then call decode(_:from:) on your Codable model – no manual CodingKeys or ad‑hoc parsing is required for flat payloads.
How it works
The decoder first applies the key strategy, turning JSON keys like created_at into the Swift property name createdAt. After key matching, the date strategy is invoked for every Date property. The closure receives a Decoder, extracts the single string value, and attempts to parse it with a formatter that understands fractional seconds. If parsing fails, the closure throws a DecodingError.dataCorrupted error, which propagates up to the caller.
Worked configuration
import Foundation
// Example model
struct Event: Codable {
let id: Int
let name: String
let createdAt: Date // maps from "created_at"
let updatedAt: Date // maps from "updated_at"
}
func makeDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
// Note: .withFractionalSeconds requires iOS 11 / macOS 10.13 or later.
decoder.dateDecodingStrategy = { dec in
let container = try dec.singleValueContainer()
let string = try container.decode(String.self)
guard let date = iso.date(from: string) else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Expected ISO 8601 date with optional fractional seconds")
}
return date
}
return decoder
}
// Usage
let jsonData = """
{
"id": 42,
"name": "Launch",
"created_at": "2026-10-19T01:19:02.887Z",
"updated_at": "2026-10-19T01:20:15.000Z"
}
""".data(using: .utf8)!
let decoder = makeDecoder()
if let event = try? decoder.decode(Event.self, from: jsonData) {
// event.createdAt and event.updatedAt are now Date instances
print(event)
} else {
// Handle decoding failure
}
Limits and common mistakes
- Key strategy only renames keys – it does not transform values, handle hyphenated keys, or resolve collisions where two JSON keys normalize to the same Swift property (e.g.,
user_idanduserIdboth becomeuserId). Such collisions trigger a duplicate‑keyCodingKeyerror. - Date strategy is global – the closure you set applies to every
Dateproperty decoded by that decoder. If your payload mixes ISO strings with epoch numbers or other formats, you need a custominit(from:)or a wrapper type. - Fractional‑second support requires a recent OS –
ISO8601DateFormatter’s.withFractionalSecondsoption is unavailable before iOS 11 / macOS 10.13. On older systems guard with#availableor fall back to a customDateFormatterpattern. - Cross‑platform variations –
swift-corelibs-foundationon Linux and Windows has historically lagged in formatter option support and may exhibit subtle differences inJSONDecoderbehavior. Verify decoding on each target you support. - Reusing a decoder – if you share a single
JSONDecoderinstance across endpoints, the date strategy you set last will affect all subsequent decodes. For per‑endpoint formats, create a fresh decoder or reset the strategy before each use.
Practical verification
Add a unit test that decodes a fixture containing snake_case keys and a timestamp with fractional seconds, then assert the resulting Date matches the expected value. Run the test on your minimum deployment target and, if the code is cross‑platform, on Linux/Windows CI. After configuring the decoder, you can also assert decoder.keyDecodingStrategy == .convertFromSnakeCase and that the date strategy closure is set before the first decode call to catch ordering bugs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.