Codable (JSON)
Codable is Swifts unified protocol for encoding and decoding values to and from external formats. Conform a type to Codable (or both Encodable + Decodable) and the compiler synthesises the JSON mapping. Customise with CodingKeys, custom init(from:), and the JSONDecoder configuration (date strategies, key strategies) to handle messy APIs.
Decode JSON, handle snake_case, dates, and unions
EXAMPLE
import Foundation
// 1) Basic Codable conformance — fields map by name
struct Money: Codable, Equatable {
let amount: Int
let currency: String
}
// 2) Snake_case from the API mapped to camelCase in Swift
struct Order: Codable {
let id: String
let customer: String
let total: Money
let createdAt: Date // mapped from "created_at" by the decoder strategy
let notes: String? // optional => key may be missing or null
}
let json = """
[
{ "id": "o1", "customer": "alice",
"total": {"amount": 4995, "currency": "AUD"},
"created_at": "2026-06-11T09:30:00Z" },
{ "id": "o2", "customer": "bob",
"total": {"amount": 0, "currency": "AUD"},
"created_at": "2026-06-11T10:05:00Z", "notes": "free sample" }
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
let orders = try decoder.decode([Order].self, from: json)
print(orders)
// 3) Custom CodingKeys for when the names cannot be inferred
struct Profile: Codable {
let displayName: String
let avatarURL: URL
private enum CodingKeys: String, CodingKey {
case displayName = "display_name"
case avatarURL = "avatar_url"
}
}
// 4) Sum types via a discriminator field
enum Event: Codable {
case orderPaid(orderId: String)
case orderShipped(orderId: String, tracking: String)
private enum CodingKeys: String, CodingKey { case type, orderId, tracking }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
switch try c.decode(String.self, forKey: .type) {
case "paid":
self = .orderPaid(orderId: try c.decode(String.self, forKey: .orderId))
case "shipped":
self = .orderShipped(
orderId: try c.decode(String.self, forKey: .orderId),
tracking: try c.decode(String.self, forKey: .tracking))
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: c,
debugDescription: "Unknown event type")
}
}
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .orderPaid(let id):
try c.encode("paid", forKey: .type)
try c.encode(id, forKey: .orderId)
case .orderShipped(let id, let tracking):
try c.encode("shipped", forKey: .type)
try c.encode(id, forKey: .orderId)
try c.encode(tracking, forKey: .tracking)
}
}
}
// 5) Encode back to JSON for a request body
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
let body = try encoder.encode(orders[0])
print(String(data: body, encoding: .utf8)!)
Why it matters
Set the JSONDecoders dateDecodingStrategy and keyDecodingStrategy once at the call site and your domain types stay clean — no per-property workaround for snake_case or ISO dates. The same pattern applies to JSONEncoder; configure both ends symmetrically so round-trips are lossless.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
struct User: Codable { let name: String; let age: Int }
let u = User(name: "Ada", age: 36)
let data = try JSONEncoder().encode(u)
print(String(data: data, encoding: .utf8)!)
let back = try JSONDecoder().decode(User.self, from: data)
Try it Yourself »
Exercise
Make a type encodable + decodable.
struct User:
{ let name: String }
PascalCase.
Discussion
Loading…