Swift JSON with Codable: JSONDecoder, CodingKeys & Custom Strategies
Last updated:
Swift Codable protocol — a type alias for Encodable & Decodable — enables compile-time JSON serialization: conform a struct to Codable and JSONDecoder().decode(MyType.self, from: data) handles all JSON parsing with no runtime reflection overhead. JSONDecoder decodes a 1 MB JSON payload in ~3 ms on an iPhone 15 — 5× faster than manual JSONSerialization parsing for the same struct; the decoder is not thread-safe, so create one per decode operation or protect with a mutex in concurrent contexts. This guide covers Codable struct definitions, CodingKeys enum for JSON field name mapping, keyDecodingStrategy (.convertFromSnakeCase), custom init(from:) and encode(to:), handling optional fields, AnyCodable for heterogeneous JSON, and URLSession JSON API integration.
Codable Struct Definition and JSONDecoder Basics
Conforming a struct to Codable and calling JSONDecoder().decode() is the starting point for Swift JSON parsing. The compiler automatically synthesizes init(from:) and encode(to:) when all stored properties are themselves Codable. JSONEncoder().encode() returns Data; convert to String with String(data: data, encoding: .utf8) for debugging. Wrap all decode calls in do-catch to handle DecodingError, which carries a detailed context.codingPath array identifying exactly which field caused the failure.
import Foundation
// ── 1. Define a Codable struct ─────────────────────────────────
struct User: Codable {
let id: Int
let name: String
let email: String
let age: Int? // Optional — nil if key is missing or null
}
// ── 2. Decode JSON Data → Swift struct ────────────────────────
let json = """
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"age": 30
}
""".data(using: .utf8)!
do {
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: json)
print(user.name) // Alice
} catch let DecodingError.keyNotFound(key, context) {
print("Missing key: \(key.stringValue) at \(context.codingPath)")
} catch let DecodingError.typeMismatch(type, context) {
print("Type mismatch: expected \(type) at \(context.codingPath)")
} catch let DecodingError.valueNotFound(type, context) {
print("Null value for non-optional: \(type) at \(context.codingPath)")
} catch let DecodingError.dataCorrupted(context) {
print("Corrupted data: \(context.debugDescription)")
}
// ── 3. Encode Swift struct → JSON Data ────────────────────────
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let encodedData = try encoder.encode(user)
let jsonString = String(data: encodedData, encoding: .utf8)!
// {
// "age": 30,
// "email": "alice@example.com",
// "id": 1,
// "name": "Alice"
// }
// ── 4. Decode JSON array ───────────────────────────────────────
let arrayJson = """
[{"id":1,"name":"Alice","email":"a@b.com"},
{"id":2,"name":"Bob","email":"b@c.com"}]
""".data(using: .utf8)!
let users = try JSONDecoder().decode([User].self, from: arrayJson)
print(users.count) // 2
// ── 5. Nested struct — synthesized automatically ───────────────
struct Address: Codable {
let street: String
let city: String
let country: String
}
struct UserWithAddress: Codable {
let id: Int
let name: String
let address: Address // nested — decoder handles recursively
}DecodingError carries a codingPath array on every case — for a deeply nested failure like users[2].address.city, the path will be [users, Index 2, address, city], making it straightforward to log exactly where decoding failed. Use encoder.outputFormatting = .prettyPrinted during development for readable output; omit it in production to minimize JSON payload size.
CodingKeys: Custom JSON Field Name Mapping
CodingKeys is a nested enum conforming to String, CodingKey that maps Swift property names to JSON key strings. Define it when JSON keys differ from Swift naming conventions, when you need to exclude properties from encoding, or when working with nested containers. A critical rule: once you define CodingKeys, you must list every property you want encoded/decoded — omitting a property from CodingKeys silently excludes it.
// ── 1. Basic CodingKeys — map snake_case JSON to camelCase Swift ─
struct Article: Codable {
let id: Int
let title: String
let authorName: String // JSON key: "author_name"
let createdAt: Date // JSON key: "created_at"
let viewCount: Int // JSON key: "view_count"
enum CodingKeys: String, CodingKey {
case id
case title
case authorName = "author_name"
case createdAt = "created_at"
case viewCount = "view_count"
}
}
// ── 2. Exclude a property from encoding (computed/transient) ────
struct Product: Codable {
let id: Int
let name: String
let price: Double
// 'discountedPrice' is computed — exclude from Codable
var discountedPrice: Double { price * 0.9 }
enum CodingKeys: String, CodingKey {
case id, name, price
// discountedPrice intentionally omitted → not encoded/decoded
}
}
// ── 3. Nested CodingKeys — flatten a nested JSON object ─────────
// JSON: { "id": 1, "meta": { "views": 100, "likes": 50 } }
// Swift: flat struct with id, views, likes
struct Post: Codable {
let id: Int
let views: Int
let likes: Int
enum CodingKeys: String, CodingKey { case id, meta }
enum MetaKeys: String, CodingKey { case views, likes }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(Int.self, forKey: .id)
let meta = try c.nestedContainer(keyedBy: MetaKeys.self, forKey: .meta)
views = try meta.decode(Int.self, forKey: .views)
likes = try meta.decode(Int.self, forKey: .likes)
}
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(id, forKey: .id)
var meta = c.nestedContainer(keyedBy: MetaKeys.self, forKey: .meta)
try meta.encode(views, forKey: .views)
try meta.encode(likes, forKey: .likes)
}
}
// ── 4. CodingKeys inheritance caveat ────────────────────────────
// Subclasses CANNOT inherit CodingKeys — must redeclare all keys
class Animal: Codable {
let name: String
enum CodingKeys: String, CodingKey { case name }
}
class Dog: Animal {
let breed: String
// Must redeclare ALL keys including 'name' from the superclass
enum CodingKeys: String, CodingKey { case name, breed }
required init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
breed = try c.decode(String.self, forKey: .breed)
try super.init(from: decoder) // delegates to Animal's init
}
}When using CodingKeys alongside keyDecodingStrategy = .convertFromSnakeCase, the CodingKeys raw string values take precedence — the strategy only applies to keys not covered by CodingKeys. This lets you use .convertFromSnakeCase for most fields while defining CodingKeys only for the exceptions.
keyDecodingStrategy and dateDecodingStrategy
keyDecodingStrategy and dateDecodingStrategy are JSONDecoder properties that transform keys and dates globally, eliminating the need for per-field CodingKeys entries. .convertFromSnakeCase is the most common key strategy — it handles the majority of REST API responses where the server uses snake_case. Combining key and date strategies on a single decoder instance applies both transformations in one pass.
import Foundation
// ── 1. .convertFromSnakeCase — no CodingKeys needed ─────────────
struct Event: Codable {
let eventId: Int // JSON: "event_id"
let eventName: String // JSON: "event_name"
let startTime: Date // JSON: "start_time"
let maxAttendees: Int // JSON: "max_attendees"
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
let json = """
{
"event_id": 42,
"event_name": "WWDC 2026",
"start_time": "2026-06-08T09:00:00Z",
"max_attendees": 5000
}
""".data(using: .utf8)!
let event = try decoder.decode(Event.self, from: json)
print(event.eventName) // WWDC 2026
print(event.startTime) // 2026-06-08 09:00:00 +0000
// ── 2. Date strategies compared ──────────────────────────────────
// .iso8601 → "2026-05-20T10:00:00Z" (no fractional seconds)
// .secondsSince1970 → 1747742400 (JSON number)
// .millisecondsSince1970 → 1747742400000 (JSON number)
// .formatted(dateFormatter) → custom string format
// .custom(closure) → full control
// .iso8601 does NOT handle fractional seconds — use .formatted instead:
let isoFormatter = DateFormatter()
isoFormatter.locale = Locale(identifier: "en_US_POSIX")
isoFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
isoFormatter.timeZone = TimeZone(secondsFromGMT: 0)
let decoderWithMs = JSONDecoder()
decoderWithMs.keyDecodingStrategy = .convertFromSnakeCase
decoderWithMs.dateDecodingStrategy = .formatted(isoFormatter)
// ── 3. Custom key decoding strategy ─────────────────────────────
// Map kebab-case JSON keys (less common) to camelCase Swift
let customDecoder = JSONDecoder()
customDecoder.keyDecodingStrategy = .custom { codingPath in
let last = codingPath.last!.stringValue
let camel = last.split(separator: "-").enumerated().map { idx, part in
idx == 0 ? String(part) : String(part).capitalized
}.joined()
return AnyKey(stringValue: camel)!
}
// Helper for custom key strategy
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
}
// ── 4. Encoder key strategy — mirror for round-tripping ─────────
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
// Encodes { event_id, event_name, start_time, max_attendees }
let encoded = try encoder.encode(event)A common pitfall: Swift's built-in .iso8601 date strategy was implemented before the ISO 8601 standard added fractional seconds support. APIs that return "2026-05-20T10:00:00.000Z" will throw DecodingError.dataCorrupted with .iso8601 — use .formatted with the SSSZ format string instead. See the JSON performance guide for benchmarks comparing decoder configuration overhead.
Custom init(from:) and encode(to:) for Complex JSON
Custom init(from decoder: Decoder) and encode(to encoder: Encoder) handle JSON shapes that cannot be expressed with synthesized conformance: flattened nested objects, computed properties, asymmetric encode/decode, and JSON arrays at the top level. Implementing either disables synthesis for that direction — if you implement init(from:), Swift no longer synthesizes it, but encode(to:) is still synthesized unless you also implement it.
// ── 1. Custom init — decode + provide defaults ───────────────────
struct Config: Codable {
let host: String
let port: Int
let useTLS: Bool
let timeout: TimeInterval
enum CodingKeys: String, CodingKey {
case host, port
case useTLS = "use_tls"
case timeout
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
host = try c.decode(String.self, forKey: .host)
port = try c.decodeIfPresent(Int.self, forKey: .port) ?? 443
useTLS = try c.decodeIfPresent(Bool.self, forKey: .useTLS) ?? true
timeout = try c.decodeIfPresent(Double.self, forKey: .timeout) ?? 30.0
}
}
// ── 2. Asymmetric encode — encode computed shape, decode flat ────
struct Temperature: Codable {
let celsius: Double
var fahrenheit: Double { celsius * 9 / 5 + 32 }
enum CodingKeys: String, CodingKey { case celsius, fahrenheit }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
celsius = try c.decode(Double.self, forKey: .celsius)
// fahrenheit is computed — not decoded from JSON
}
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(celsius, forKey: .celsius)
try c.encode(fahrenheit, forKey: .fahrenheit) // include computed value
}
}
// ── 3. unkeyedContainer — decode a JSON array ───────────────────
// JSON: [1, 2, 3, 4, 5] (top-level array of integers)
struct IntList: Codable {
let values: [Int]
init(from decoder: Decoder) throws {
var c = try decoder.unkeyedContainer()
var result: [Int] = []
while !c.isAtEnd {
result.append(try c.decode(Int.self))
}
values = result
}
func encode(to encoder: Encoder) throws {
var c = encoder.unkeyedContainer()
for v in values { try c.encode(v) }
}
}
// ── 4. nestedUnkeyedContainer — array nested in keyed object ────
// JSON: { "tags": ["swift", "ios", "json"] }
struct TaggedItem: Codable {
let id: Int
let tags: [String]
enum CodingKeys: String, CodingKey { case id, tags }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(Int.self, forKey: .id)
var tagC = try c.nestedUnkeyedContainer(forKey: .tags)
var result: [String] = []
while !tagC.isAtEnd {
result.append(try tagC.decode(String.self))
}
tags = result
}
}When implementing init(from:), always use decodeIfPresent with a ?? default fallback for optional or defaulted fields rather than decode — this makes your model resilient to APIs that evolve by adding optional fields. container.decodeIfPresent returns nil for both missing keys and JSON null, so combine it with nil-coalescing to supply defaults in one expression.
Handling Optional Fields and Default Values
Swift Codable distinguishes between three states for a JSON field: present with a value, present with null, and missing entirely. decodeIfPresent() collapses the latter two into nil, while decode(T?.self) preserves the distinction. Providing default values for missing fields requires a custom init(from:) since synthesized Codable does not support property default values for missing keys.
// ── 1. Optional property — synthesized Codable handles it ───────
struct Profile: Codable {
let id: Int
let username: String
let bio: String? // nil if missing OR null in JSON
let avatarUrl: String? // nil if missing OR null in JSON
}
// ── 2. Default values — requires custom init ─────────────────────
struct Settings: Codable {
let theme: String
let fontSize: Int
let notificationsEnabled: Bool
let maxResults: Int
enum CodingKeys: String, CodingKey {
case theme, fontSize, notificationsEnabled, maxResults
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
theme = try c.decodeIfPresent(String.self, forKey: .theme) ?? "system"
fontSize = try c.decodeIfPresent(Int.self, forKey: .fontSize) ?? 16
notificationsEnabled = try c.decodeIfPresent(Bool.self, forKey: .notificationsEnabled) ?? true
maxResults = try c.decodeIfPresent(Int.self, forKey: .maxResults) ?? 20
}
}
// ── 3. Distinguish null from missing key ──────────────────────────
// JSON A: { "nickname": null } — explicit null
// JSON B: { } — key absent
struct UserDetail: Decodable {
enum NicknameState {
case present(String)
case explicitNull
case missing
}
let nicknameState: NicknameState
enum CodingKeys: String, CodingKey { case nickname }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
if c.contains(.nickname) {
// Key is present — check if it's null
if let name = try c.decodeIfPresent(String.self, forKey: .nickname) {
nicknameState = .present(name)
} else {
nicknameState = .explicitNull // key present, value is null
}
} else {
nicknameState = .missing // key entirely absent
}
}
}
// ── 4. Non-optional with null guard ──────────────────────────────
// Treat null as a decoding failure (throw) for required fields
struct StrictUser: Decodable {
let id: Int
let name: String // must not be null
enum CodingKeys: String, CodingKey { case id, name }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(Int.self, forKey: .id)
// decode(String.self) throws .valueNotFound if value is null
name = try c.decode(String.self, forKey: .name)
}
}A practical rule of thumb: prefer Optional Swift properties with decodeIfPresent for any API field that might not be present in all versions of the response — API contracts evolve, and lenient decoding prevents your app from crashing when the server omits a previously-required field. Reserve strict decode() for identifier fields (id, type) where absence truly indicates a corrupt or wrong response.
AnyCodable and Heterogeneous JSON
Some JSON APIs return fields whose type varies at runtime — a value field that can be a string, number, boolean, array, or object depending on context. Swift's type system requires a concrete type for Codable, so truly dynamic JSON requires a workaround: AnyCodable (a community-maintained type), a JSONValue enum with associated values, or falling back to JSONSerialization.
// ── 1. JSONValue enum — typed heterogeneous JSON ─────────────────
// No third-party dependency — implement in your codebase
indirect enum JSONValue: Codable {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case array([JSONValue])
case object([String: JSONValue])
case null
init(from decoder: Decoder) throws {
let c = try decoder.singleValueContainer()
if c.decodeNil() { self = .null }
else if let b = try? c.decode(Bool.self) { self = .bool(b) }
else if let i = try? c.decode(Int.self) { self = .int(i) }
else if let d = try? c.decode(Double.self){ self = .double(d) }
else if let s = try? c.decode(String.self){ self = .string(s) }
else if let a = try? c.decode([JSONValue].self) { self = .array(a) }
else if let o = try? c.decode([String: JSONValue].self) { self = .object(o) }
else { throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown JSON type")) }
}
func encode(to encoder: Encoder) throws {
var c = encoder.singleValueContainer()
switch self {
case .null: try c.encodeNil()
case .bool(let b): try c.encode(b)
case .int(let i): try c.encode(i)
case .double(let d):try c.encode(d)
case .string(let s):try c.encode(s)
case .array(let a): try c.encode(a)
case .object(let o):try c.encode(o)
}
}
}
// ── 2. Use JSONValue in a Codable struct ─────────────────────────
struct EventPayload: Codable {
let eventType: String
let metadata: [String: JSONValue] // arbitrary key-value map
}
let json = """
{
"event_type": "purchase",
"metadata": {
"amount": 49.99,
"currency": "USD",
"items": ["shirt", "hat"],
"gift": true,
"coupon": null
}
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let payload = try decoder.decode(EventPayload.self, from: json)
if case .double(let amount) = payload.metadata["amount"] {
print(amount) // 49.99
}
// ── 3. JSONSerialization fallback — for truly opaque JSON ────────
// Use when you need [String: Any] and don't control the type
let raw = try JSONSerialization.jsonObject(with: json, options: [])
if let dict = raw as? [String: Any] {
print(dict["event_type"] as? String ?? "")
}
// ── 4. AnyCodable via open-source library (AnyCodable-FlightSchool)
// import AnyCodable
// struct Flexible: Codable { let data: AnyCodable }
// Wraps Any values — use when JSONValue enum is too verboseThe JSONValue enum approach is preferred over JSONSerialization for codebases that need to stay within Swift's type system and avoid Any casts. Note that Bool must be decoded before Int and Double in the init(from:) implementation — in some JSON decoder implementations, true can be coerced to Int(1) if the integer branch runs first. See the TypeScript JSON types guide for the TypeScript equivalent using discriminated unions.
URLSession JSON API Integration
Combining URLSession async/await with JSONDecoder produces clean, type-safe API clients in Swift. The pattern: fetch Data from a URL, check the HTTP status code, decode success or error responses, and propagate typed errors up the call stack. A generic APIClient eliminates boilerplate across all endpoints.
import Foundation
// ── 1. Simple GET request with async/await ───────────────────────
struct Post: Codable {
let id: Int
let title: String
let body: String
let userId: Int
}
func fetchPost(id: Int) async throws -> Post {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return try decoder.decode(Post.self, from: data)
}
// Usage: let post = try await fetchPost(id: 1)
// ── 2. POST request with JSON body ───────────────────────────────
struct CreatePostRequest: Encodable {
let title: String
let body: String
let userId: Int
}
func createPost(_ payload: CreatePostRequest) async throws -> Post {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
request.httpBody = try encoder.encode(payload)
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse,
(200..<300).contains(http.statusCode) else {
// Try to decode an error response body
let apiError = try? JSONDecoder().decode(APIError.self, from: data)
throw apiError ?? URLError(.badServerResponse)
}
return try JSONDecoder().decode(Post.self, from: data)
}
// ── 3. Error response model ───────────────────────────────────────
struct APIError: Codable, LocalizedError {
let code: String
let message: String
var errorDescription: String? { "\(code): \(message)" }
}
// ── 4. Generic APIClient ──────────────────────────────────────────
struct APIClient {
private let baseURL: URL
private let decoder: JSONDecoder
private let encoder: JSONEncoder
init(baseURL: URL) {
self.baseURL = baseURL
decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
}
func get<T: Decodable>(_ path: String) async throws -> T {
let url = baseURL.appendingPathComponent(path)
let (data, response) = try await URLSession.shared.data(from: url)
try validate(response: response, data: data)
return try decoder.decode(T.self, from: data)
}
func post<Body: Encodable, Response: Decodable>(
_ path: String,
body: Body
) async throws -> Response {
let url = baseURL.appendingPathComponent(path)
var req = URLRequest(url: url)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try encoder.encode(body)
let (data, response) = try await URLSession.shared.data(for: req)
try validate(response: response, data: data)
return try decoder.decode(Response.self, from: data)
}
private func validate(response: URLResponse, data: Data) throws {
guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) }
guard (200..<300).contains(http.statusCode) else {
if let apiError = try? decoder.decode(APIError.self, from: data) { throw apiError }
throw URLError(.badServerResponse)
}
}
}
// Usage
// let client = APIClient(baseURL: URL(string: "https://api.example.com")!)
// let post: Post = try await client.get("posts/1")
// let newPost: Post = try await client.post("posts", body: CreatePostRequest(title: "Hi", body: "Content", userId: 1))For iOS targets below iOS 15, replace URLSession.shared.data(from:) with URLSession.shared.dataTask(with:completionHandler:) wrapped in a withCheckedThrowingContinuation to bridge the callback-based API into async/await. The generic APIClient pattern keeps JSONDecoder configuration centralized — change keyDecodingStrategy or dateDecodingStrategy in one place and all endpoints benefit. See the JSON API design guide for REST response structure conventions and the Java JSON with Jackson guide for the equivalent Java pattern.
Key Terms
- Codable
- A Swift type alias for
Encodable & Decodable— a type conforming toCodablecan be both encoded to and decoded from an external representation such as JSON. The Swift compiler synthesizesinit(from decoder: Decoder)andencode(to encoder: Encoder)implementations at compile time when all stored properties are themselvesCodable, requiring no runtime reflection. This compile-time synthesis is what makes Swift Codable significantly faster than Objective-CNSJSONSerialization, which performs runtime key-value coding inspection. Types that cannot use synthesis (due to computed properties, heterogeneous values, or complex nesting) implement the protocol methods manually. - CodingKeys
- A nested
enumconforming toString, CodingKeythat maps Swift property names to their corresponding JSON key strings. When defined inside aCodabletype, it overrides the default behavior of using the Swift property name as the JSON key. Every property intended to be encoded or decoded must appear inCodingKeys— omitting a property silently excludes it from serialization.CodingKeysare used by both the synthesized and manually implementedinit(from:)andencode(to:)methods. Subclasses cannot inherit a superclass'sCodingKeysand must redeclare all keys including those from the parent class. - keyDecodingStrategy
- A
JSONDecoderproperty (of typeJSONDecoder.KeyDecodingStrategy) that applies a transformation to JSON keys before matching them to Swift property names. The most useful value is.convertFromSnakeCase, which convertssnake_caseJSON keys tocamelCaseSwift names by splitting on underscores and capitalizing the first letter of each subsequent segment. The correspondingJSONEncoderproperty iskeyEncodingStrategywith.convertToSnakeCase. A custom strategy can be provided via.custom([CodingKey] -> CodingKey)for non-standard naming conventions such as kebab-case or PascalCase JSON keys. - DecodingError
- A Swift enum (conforming to
Error) thrown byJSONDecoder.decode()when JSON cannot be decoded into the target type. It has four cases:.typeMismatch(Any.Type, Context)when a JSON value has the wrong type (e.g., a string where an integer is expected);.valueNotFound(Any.Type, Context)when a non-optional Swift property receives a JSONnull;.keyNotFound(CodingKey, Context)when a required key is absent from the JSON object; and.dataCorrupted(Context)for malformed JSON, invalid date strings, or other format violations. Each case'sContextcontains acodingPatharray — the sequence of keys and array indexes from the root to the failing field — which is invaluable for debugging decoding failures in deeply nested structures. - AnyCodable
- A community-maintained Swift type (most commonly from the
AnyCodable-FlightSchoolpackage by Mattt) that wraps an arbitrary value (Any) and conforms toCodable, enabling encoding and decoding of heterogeneous JSON values where the type is unknown at compile time. It implementsinit(from:)by attempting to decode each JSON scalar type in turn (bool, int, double, string, array, dictionary, null) andencode(to:)by switching on the wrapped value's type. An equivalent pattern that avoids a third-party dependency is a customJSONValueenum with associated values for each JSON type — this approach provides type safety and pattern matching without depending onAny. - unkeyedContainer
- A decoding container (conforming to
UnkeyedDecodingContainer) returned bydecoder.unkeyedContainer()orcontainer.nestedUnkeyedContainer(forKey:)for decoding JSON arrays. It provides sequential access to array elements viadecode(T.self)anddecodeIfPresent(T.self), advancing an internal cursor with each call. TheisAtEndproperty signals when all elements have been consumed. UseunkeyedContainerwhen the top-level JSON value is an array (instead of an object), or when a keyed field contains a JSON array that requires custom decoding logic per element. The corresponding encoding container isUnkeyedEncodingContainer, obtained viaencoder.unkeyedContainer().
FAQ
How do I decode JSON in Swift using Codable?
Conform your struct to Codable, then call JSONDecoder().decode(MyType.self, from: data) where data is a Data object containing the JSON bytes. The compiler synthesizes the decode implementation when all stored properties are themselves Codable. Wrap the call in a do-catch block to handle DecodingError — the four cases are .typeMismatch, .valueNotFound, .keyNotFound, and .dataCorrupted, each carrying a codingPath that identifies exactly which field caused the failure. To decode a JSON string rather than Data, first convert: let data = jsonString.data(using: .utf8)!. JSONDecoder is not thread-safe — create a new instance per decode call in concurrent contexts.
How do I map snake_case JSON keys to camelCase Swift properties?
Set JSONDecoder().keyDecodingStrategy = .convertFromSnakeCase before calling decode(). This automatically maps user_name to userName, created_at to createdAt, and so on — no CodingKeys enum needed for straightforward renaming. The strategy splits on underscores and capitalizes the following letter. For keys with all-caps abbreviations (e.g., xml_parser becomes xmlParser, but XMLParser would not map correctly), define a CodingKeys entry for those specific fields. Use the mirror strategy on the encoder side: JSONEncoder().keyEncodingStrategy = .convertToSnakeCase for round-tripping data back to a snake_case API.
How do I handle optional JSON fields in Swift?
Declare the Swift property as Optional (e.g., var nickname: String?). With synthesized Codable, Swift treats optional properties leniently — both a missing key and a JSON null value result in the property being set to nil. In a custom init(from:), use container.decodeIfPresent(String.self, forKey: .nickname) which returns nil for both missing keys and JSON null without throwing. To provide a default value for a missing field, combine decodeIfPresent with nil-coalescing: nickname = try c.decodeIfPresent(String.self, forKey: .nickname) ?? "Guest". Synthesized Codable does not support property-level default values for missing keys — a custom init(from:) is required for defaults.
What is the difference between decode() and decodeIfPresent() in Swift?
decode(T.self, forKey:) requires the key to be present and the value to be non-null — it throws DecodingError.keyNotFound for absent keys and DecodingError.valueNotFound for null values when T is non-optional. decodeIfPresent(T.self, forKey:) returns nil for both missing keys and JSON null values without throwing — it is the idiomatic choice for optional fields. A third variant, decode(T?.self, forKey:), returns nil for JSON null but throws .keyNotFound for absent keys — useful when you need to distinguish between an explicitly null value and a missing key. Use decode() for required fields, decodeIfPresent() for optional fields, and decode(Optional.self) only when the null/missing distinction matters.
How do I decode JSON dates in Swift?
Set JSONDecoder().dateDecodingStrategy before calling decode(). For ISO 8601 strings without fractional seconds (e.g., "2026-05-20T10:00:00Z"), use .iso8601. For Unix timestamps, use .secondsSince1970 (seconds as a JSON number) or .millisecondsSince1970 (milliseconds). For ISO 8601 with fractional seconds (e.g., "2026-05-20T10:00:00.000Z"), use .formatted with a DateFormatter configured with dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" and locale = Locale(identifier: "en_US_POSIX"). For any other format, use .custom with a closure that accepts a Decoder and returns a Date. A mismatched strategy throws DecodingError.dataCorrupted.
How do I decode nested JSON objects in Swift?
For nested objects that map to separate Swift structs, define nested Codable structs — the synthesized decoder handles the nesting recursively with no extra code. For flattening a nested JSON object into a parent struct (e.g., promoting {"meta":{"id":1}} fields into the parent), implement a custom init(from:) and use container.nestedContainer(keyedBy: MetaKeys.self, forKey: .meta) to obtain a sub-container for the nested object, then decode its fields individually. For nested JSON arrays, use container.nestedUnkeyedContainer(forKey:) and iterate with isAtEnd. Nesting depth is unlimited — each level of nesting corresponds to one nestedContainer call.
Is JSONDecoder thread-safe in Swift?
No — JSONDecoder is NOT thread-safe. Calling decode() concurrently on the same JSONDecoder instance from multiple threads or async tasks causes data races and undefined behavior. The correct pattern is to create a new JSONDecoder() instance inside each decode call site, or to create one per async Task. If you have a helper that configures a decoder with specific strategies, make it a factory function that returns a fresh instance rather than a shared property. JSONEncoder, by contrast, became thread-safe in Swift 5.3 — a single configured JSONEncoder can be safely shared and called concurrently. The performance overhead of allocating a new JSONDecoder is negligible (microseconds) compared to the actual JSON parsing cost.
How do I call a JSON API in Swift with URLSession?
Use URLSession.shared.data(from: url) with async/await (iOS 15+ / macOS 12+): let (data, response) = try await URLSession.shared.data(from: url). Cast the response to HTTPURLResponse and check the status code: guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw URLError(.badServerResponse) }. Then decode: let result = try JSONDecoder().decode(MyType.self, from: data). For POST requests, create a URLRequest, set httpMethod = "POST", set the Content-Type: application/json header, and assign httpBody = try JSONEncoder().encode(payload). For 4xx/5xx responses, attempt to decode an error response model from the response body before falling back to a generic error. For pre-iOS 15 targets, wrap dataTask(with:completionHandler:) in withCheckedThrowingContinuation.
Further reading and primary sources
- Swift Documentation: Encoding and Decoding Custom Types — Official Apple guide on Codable, CodingKeys, and custom encode/decode implementations
- JSONDecoder — Apple Developer Documentation — Full API reference for JSONDecoder including all decoding strategies and their behavior
- Swift Evolution SE-0166: Swift Archival and Serialization — Original Swift Evolution proposal that introduced the Codable protocol and its design rationale
- URLSession — Apple Developer Documentation — URLSession async/await API reference for data(from:) and data(for:) methods
- AnyCodable on GitHub (Flight-School) — Community AnyCodable implementation for encoding and decoding heterogeneous JSON values