Codable and Serialization
|
This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Standard Library Overview introduced Codable
in passing; this page covers converting Swift values to and from an external representation — JSON, property
lists, and beyond — in depth: synthesized conformance, CodingKeys, JSONEncoder/JSONDecoder, and the manual
init(from:)/encode(to:) implementations needed once synthesis alone is not enough.
Encodable, Decodable, Codable
protocol Encodable {
func encode(to encoder: Encoder) throws
}
protocol Decodable {
init(from decoder: Decoder) throws
}
typealias Codable = Encodable & Decodable
Codable is nothing more than that type alias: a type conforming to both directions. Most types never write
encode(to:)/init(from:) by hand at all — the compiler synthesizes both whenever every stored property is
itself Codable:
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
var label: String?
}
CodingKeys
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
var label: String?
enum CodingKeys: String, CodingKey {
case latitude = "lat" // renamed on the wire
case longitude = "lng" // renamed on the wire
case label // same name on the wire
// omitting a stored property from CodingKeys excludes it from encoding/decoding entirely
}
}
A nested CodingKeys enum (raw type String, conforming to CodingKey) renames properties for the encoded
form and/or drops properties that should never be encoded/decoded — the compiler still synthesizes
encode(to:)/init(from:) around it, as long as every included case matches a stored property.
JSONEncoder and JSONDecoder
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
var recordedAt: Date
}
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.keyEncodingStrategy = .convertToSnakeCase // recordedAt -> recorded_at
encoder.dateEncodingStrategy = .iso8601
let data = try encoder.encode(Coordinate(latitude: 41.9, longitude: 12.5, recordedAt: .now))
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
let roundTripped = try decoder.decode(Coordinate.self, from: data)
| Strategy | Controls |
|---|---|
|
Property-name casing on the wire ( |
|
|
|
|
|
How |
A decoding failure throws a DecodingError case (.keyNotFound, .typeMismatch, .valueNotFound,
.dataCorrupted), each carrying a Context with a codingPath pinpointing where decoding went wrong — see
Error Handling for handling thrown errors in general.
Custom init(from:) / encode(to:)
When a type’s wire format does not match its Swift representation one-to-one, implement the two methods
directly using containers, obtained from the Encoder/Decoder the synthesized code would otherwise use
internally:
struct Temperature: Codable {
var celsius: Double
enum CodingKeys: String, CodingKey {
case value, unit
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) // KeyedEncodingContainer
try container.encode(celsius, forKey: .value)
try container.encode("celsius", forKey: .unit)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) // KeyedDecodingContainer
let value = try container.decode(Double.self, forKey: .value)
let unit = try container.decode(String.self, forKey: .unit)
celsius = unit == "fahrenheit" ? (value - 32) / 1.8 : value
}
}
| Container | Shape | Obtained with |
|---|---|---|
|
A dictionary-like |
|
|
An ordered, array-like sequence — decode by
repeatedly calling |
|
|
One bare scalar, with no surrounding object or array. |
|
struct RGB: Codable { // encodes as a bare [Int] rather than a keyed object
var red: Int, green: Int, blue: Int
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(red)
try container.encode(green)
try container.encode(blue)
}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
red = try container.decode(Int.self)
green = try container.decode(Int.self)
blue = try container.decode(Int.self)
}
}
Nested Containers
struct Placemark: Codable {
var name: String
var latitude: Double
var longitude: Double
enum CodingKeys: String, CodingKey { case name, location }
enum LocationKeys: String, CodingKey { case latitude, longitude }
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
var locationContainer = container.nestedContainer(keyedBy: LocationKeys.self, forKey: .location)
try locationContainer.encode(latitude, forKey: .latitude)
try locationContainer.encode(longitude, forKey: .longitude)
}
}
// Produces: {"name": "Rome", "location": {"latitude": 41.9, "longitude": 12.5}}
nestedContainer(keyedBy:forKey:)/nestedUnkeyedContainer(forKey:) let a flat Swift struct encode as a nested
JSON object, or vice versa — the Swift shape and the wire shape need not match.
Polymorphic and Enum-with-Associated-Values Encoding
Neither protocols nor enums with associated values get synthesized Codable conformance automatically; both
need a discriminator field written and read by hand:
enum Shape: Codable {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
private enum CodingKeys: String, CodingKey { case kind, radius, width, height }
private enum Kind: String, Codable { case circle, rectangle }
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .circle(let radius):
try container.encode(Kind.circle, forKey: .kind)
try container.encode(radius, forKey: .radius)
case .rectangle(let width, let height):
try container.encode(Kind.rectangle, forKey: .kind)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
switch try container.decode(Kind.self, forKey: .kind) {
case .circle:
self = .circle(radius: try container.decode(Double.self, forKey: .radius))
case .rectangle:
self = .rectangle(
width: try container.decode(Double.self, forKey: .width),
height: try container.decode(Double.self, forKey: .height))
}
}
}
The same discriminator-field technique decodes a heterogeneous array of protocol-typed values: decode the
discriminator first inside a loop over an unkeyed container, then dispatch to the concrete type’s own
init(from:).
PropertyListEncoder
let plistEncoder = PropertyListEncoder()
plistEncoder.outputFormat = .xml // or .binary
let plistData = try plistEncoder.encode(Coordinate(latitude: 41.9, longitude: 12.5, recordedAt: .now))
let plistDecoder = PropertyListDecoder()
let decoded = try plistDecoder.decode(Coordinate.self, from: plistData)
PropertyListEncoder/PropertyListDecoder implement the same Codable protocol as their JSON counterparts, so
any type already conforming to Codable for JSON works with property lists (.plist files, UserDefaults
values) with no changes.
Parsing XML with Foundation’s XMLParser
XMLParser is event-driven (SAX-style), not Codable-based — there is no built-in XML coder, so an XML
document is parsed by implementing XMLParserDelegate and reacting to callbacks as the parser streams through
the document:
final class PlacemarkParser: NSObject, XMLParserDelegate {
var names: [String] = []
private var currentElement = ""
private var currentText = ""
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?,
qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
currentElement = elementName
currentText = ""
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
currentText += string
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?,
qualifiedName qName: String?) {
if elementName == "name" {
names.append(currentText.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
}
let parser = XMLParser(data: xmlData)
let delegate = PlacemarkParser()
parser.delegate = delegate
parser.parse()
Reach for XMLParser for large documents processed as a stream, or when there is genuinely no Codable path;
for anything JSON-shaped, JSONEncoder/JSONDecoder remain the far less code-heavy choice.
See Also
-
Standard Library Overview — where
Codablewas first introduced. -
Protocols — synthesized
Equatable/Hashable/Comparable/Codableconformance in general. -
Error Handling — handling
DecodingErrorand other thrown errors. -
Foundation Essentials —
Date,Dataand the other Foundation typesCodablestrategies convert to and from.