How does Codable work? How do you handle keys and types that do not match your model?
EasyAsked oftenNetworkingSwift 1 min read
TL;DR
Codable is Encodable & Decodable. If all properties are Codable, the compiler writes the code for you.
When JSON does not match your model, use (in this order): decoder strategies, CodingKeys, then a custom init(from:).
Optional properties are fine when a key may be missing.
The basic case
struct Movie: Codable {
let id: Int
let title: String
let rating: Double?
}
let json = #"{"id": 1, "title": "Dune", "rating": 8.1}"#.data(using: .utf8)!
let movie = try JSONDecoder().decode(Movie.self, from: json)If rating is missing or null, it becomes nil. If title is missing, decoding throws.
Decoder strategies
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // release_date -> releaseDate
decoder.dateDecodingStrategy = .iso8601 // "2026-01-31T10:00:00Z"These fix the whole payload with one line. Create the decoder once and reuse it.
CodingKeys for different names
struct Movie: Codable {
let id: Int
let title: String
let posterURL: URL
enum CodingKeys: String, CodingKey {
case id
case title = "original_title"
case posterURL = "poster_path"
}
}Leave a property out of CodingKeys to skip it. It then needs a default value.
Custom init(from:)
Use it when types or structure do not match.
struct Product: Decodable {
let id: String
let price: Decimal
let tags: [String]
enum CodingKeys: String, CodingKey { case id, price, tags }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
// id can be a number or a string
if let intID = try? c.decode(Int.self, forKey: .id) {
id = String(intID)
} else {
id = try c.decode(String.self, forKey: .id)
}
// price comes as a string like "19.99"
let priceText = try c.decode(String.self, forKey: .price)
price = Decimal(string: priceText) ?? 0
// tags may be missing
tags = try c.decodeIfPresent([String].self, forKey: .tags) ?? []
}
}Nested JSON
// {"data": {"user": {"name": "Ana"}}}
struct Envelope<T: Decodable>: Decodable { let data: T }
struct UserBox: Decodable { let user: User }
let name = try decoder.decode(Envelope<UserBox>.self, from: json).data.user.nameA generic wrapper is often simpler than nestedContainer.
Enums
enum Status: String, Codable { case active, banned }
// Swift 5.5+: associated values are synthesized too
enum Shape: Codable {
case circle(radius: Double)
case square(side: Double)
}An unknown raw value like "pending" makes decoding throw. A safe pattern:
enum Status: String, Decodable {
case active, banned, unknown
init(from decoder: Decoder) throws {
let raw = try decoder.singleValueContainer().decode(String.self)
self = Status(rawValue: raw) ?? .unknown
}
}Reading errors
do {
_ = try decoder.decode(Movie.self, from: data)
} catch let DecodingError.keyNotFound(key, context) {
print("Missing", key.stringValue, "at", context.codingPath)
} catch let DecodingError.typeMismatch(type, context) {
print("Wrong type", type, context.debugDescription)
}codingPath tells you exactly where the problem is.
Common follow-up questions
- One bad item breaks the whole array. How do you fix it? Decode each element as optional with a wrapper and drop the failures (a "lossy array").
- Where should
Codablelive: in the domain model or a separate DTO? For messy APIs, use a DTO and map it to a clean model. - Is
JSONDecoderthread-safe? Using one instance from many threads is fine in practice, but do not change its settings while it decodes. In Swift 6,JSONDecoderisSendable. - How do you encode a
Datein a custom format? SetdateEncodingStrategy = .formatted(formatter)or.custom { ... }. - Is
Codableslow? It is fine for normal APIs. For huge payloads, decode on a background task.