Simplifying JSON in Swift with Foundation's Codable
Learn how Foundation's Codable protocol eliminates manual JSON mapping in Swift, with a concrete example, configuration tips, and practical limitations.
06 May 2026, 12:22 UTC

The problem: repetitive JSON mapping code
When you receive JSON from a web service, the typical Swift approach involves writing manual init and toDictionary methods for each model. This boilerplate is tedious, error‑prone, and makes it hard to keep the Swift struct in sync with the server schema.
How Codable removes the boilerplate
Foundation’s Codable protocol combines Encodable and Decodable. By declaring a type as Codable, the compiler synthesizes the encoding and decoding logic for you, provided all stored properties are themselves Codable. You can then use JSONEncoder and JSONDecoder to move between Swift instances and JSON data.
Worked example: encoding and decoding a User model
Define a simple model that represents a user record:
import Foundation
struct User: Codable {
let id: Int
let name: String
var createdAt: Date
}
// Sample data
let user = User(id: 42, name: "Ada Lovelace", createdAt: Date())
// Configure encoder for snake_case keys and ISO8601 dates
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(user)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString) // Example output: {"id":42,"name":"Ada Lovelace","created_at":"2026-10-01T03:39:31Z"}
}
// Decode back from JSON
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
let decodedUser = try decoder.decode(User.self, from: jsonData)
print(decodedUser == user) // true
If the JSON is malformed—for instance, missing the created_at field—the decoder throws a DecodingError with a clear path indicating which property failed:
let badJSON = "{\"id\":42,\"name\":\"Ada\"}".data(using: .utf8)!
do {
_ = try decoder.decode(User.self, from: badJSON)
} catch let error as DecodingError {
switch error {
case .keyNotFound(let key, let context):
print("Missing key \(key.stringValue) at \(context.codingPath)")
default:
print("Other error: \(error)")
}
}
Trade‑offs and limitations
- All properties must be Codable. If you store a custom type that lacks
Encodable/Decodableconformance, you must add it yourself or mark the property with@objcand handle it manually. - Schema versioning. Adding a new required field breaks decoding of older JSON unless you provide a default value or make the property optional. Removing a field is safe only if the JSON never contains it for newer clients.
- Performance. The compile‑time generated code is comparable to hand‑written mapping; there is no significant runtime reflection cost.
Actionable next steps
Start by adopting Codable for new network models. Use the encoder/decoder strategies (convertToSnakeCase, iso8601) to match common backend conventions. When you encounter a legacy type that cannot be made Codable, isolate it in a wrapper and implement custom encode(to:) and init(from:) methods only for that wrapper. Finally, validate your models with unit tests that encode a known instance, decode the resulting JSON, and assert equality—this catches breaking changes early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.