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

keyEncodingStrategy / keyDecodingStrategy

Property-name casing on the wire (.convertToSnakeCase, .custom).

dateEncodingStrategy / dateDecodingStrategy

Date representation (.iso8601, .secondsSince1970, .formatted, .custom).

dataEncodingStrategy / dataDecodingStrategy

Data representation (.base64, .custom).

nonConformingFloatEncodingStrategy / …​Decoding…​

How .infinity/.nan are represented, since JSON has no native support for them.

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

KeyedEncodingContainer<Key> / KeyedDecodingContainer<Key>

A dictionary-like {key: value, …​} object, keyed by a CodingKey type.

container(keyedBy:)

UnkeyedEncodingContainer / UnkeyedDecodingContainer

An ordered, array-like sequence — decode by repeatedly calling decode until isAtEnd.

unkeyedContainer()

SingleValueEncodingContainer / SingleValueDecodingContainer

One bare scalar, with no surrounding object or array.

singleValueContainer()

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 Codable was first introduced.

  • Protocols — synthesized Equatable/Hashable/ Comparable/Codable conformance in general.

  • Error Handling — handling DecodingError and other thrown errors.

  • Foundation Essentials — Date, Data and the other Foundation types Codable strategies convert to and from.