APIs & Data Management

Chapter 3
Swift Data Modeling and Codable

Certificate in IOS Development
APIs & Data Management   •   Certificate

Swift Data Modeling and Codable

Introduction

Modern iOS applications rarely work with isolated values. They retrieve users, products, messages, orders, images, and configuration data from APIs; store some of it locally; transform it for presentation; and send updated information back to servers. The quality of these operations depends on data modeling: representing information in Swift types that are understandable, safe, and appropriate for the application domain.

Swift provides structures, classes, enumerations, optionals, collections, and protocols for modeling data. The Codable system extends these tools by allowing Swift values to be encoded to and decoded from external formats such as JSON. Used carefully, Codable reduces repetitive parsing code while preserving compile-time safety. Used carelessly, it can hide ambiguous data contracts, mishandle dates, or convert invalid external data into misleading application state.

This chapter develops a practical foundation for designing Swift models and safely transforming API data. The focus is not merely on syntax, but on decisions: when to use a structure or class, how to represent absence, how to model nested JSON, how to handle changing server contracts, and how to prevent unsafe data from entering the application.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish structures, classes, optionals, collections, and enumerations in data models;
  • design Swift types that represent API and application-domain data;
  • decode and encode JSON with Codable;
  • map irregular JSON keys with custom coding keys;
  • handle dates, nested objects, arrays, and nullable fields;
  • transform transport models into safer application models; and
  • identify common failure modes and apply appropriate validation and error handling.

1. Data Modeling as a Contract

A data model is a typed description of information and its relationships. For example, an API product may contain an identifier, title, price, stock status, and category. A Swift model makes those expectations explicit:

struct Product: Codable {
    let id: Int
    let title: String
    let price: Double
    let inStock: Bool
}

The model is also a contract between external data and application code. If the server supplies a string where the model expects an integer, decoding fails rather than silently producing an incorrect value. This behavior is valuable because external data is not automatically trustworthy. A model should make valid states easy to represent and invalid states difficult to ignore.

A useful distinction is between a transport model and a domain model. A transport model mirrors the API payload, including server-specific names and optional fields. A domain model represents what the application actually needs. The two may be identical for a small project, but separating them becomes valuable when an API is inconsistent, versioned, or controlled by another team.

2. Structures, Classes, and Value Semantics

Structures

A structure is a value type. Assigning a structure to another variable copies its value conceptually:

struct Coordinate {
    var latitude: Double
    var longitude: Double
}

var first = Coordinate(latitude: 51.5, longitude: -0.1)
var second = first
second.latitude = 52.0

// first.latitude remains 51.5

Structures are usually the default choice for data models because independent model values are generally desirable. They work naturally with Codable, are straightforward to test, and avoid shared mutable state.

Classes

A class is a reference type. Assigning an instance copies a reference to the same object:

final class Session {
    var token: String

    init(token: String) {
        self.token = token
    }
}

let original = Session(token: "abc")
let anotherReference = original
anotherReference.token = "xyz"
// original.token is now "xyz"

Classes are appropriate when identity matters, when several parts of an application must observe the same mutable object, or when inheritance is required. They should not be selected merely because a model has many properties. For API response models, structures are commonly safer.

The final modifier prevents subclassing and communicates that the class is not designed as an inheritance base. In concurrent applications, shared mutable class instances require particular care because mutations can produce race conditions.

QuestionStructureClass
Semantic categoryValue typeReference type
AssignmentIndependent valueShared reference
IdentityUsually not centralOften significant
InheritanceNot supportedSupported unless final
Typical API model useCommon defaultUse when identity or shared state matters

3. Properties, Optionals, and Valid States

An optional represents either a value or no value. It is written with ?:

struct Profile: Codable {
    let username: String
    let biography: String?
    let avatarURL: URL?
}

biography may be absent or explicitly null in the source data. It must be safely unwrapped before use:

if let biography = profile.biography {
    print(biography)
}

let displayedBiography = profile.biography ?? "No biography provided"

Optionality should reflect the data contract, not convenience. A property should be optional when the source may omit it or provide null, or when the application genuinely permits absence. Making every property optional avoids decoding failures but transfers uncertainty throughout the application. Making no properties optional falsely claims that every value is guaranteed.

A common error is force unwrapping:

let text = profile.biography!

This can terminate the application when the value is absent. Force unwrapping may be defensible after a proven invariant, but it should not be the normal response to API uncertainty.

There is an important difference between an optional property and a default value. String? preserves the difference between “missing” and “present but empty.” A default such as "" removes that distinction. The correct choice depends on domain meaning.

4. Collections and Enumerations

Collections model groups of values. The most frequently used types are:

let tags: [String] = ["swift", "ios"]
let quantities: Set<Int> = [1, 2, 3]
let ratings: [String: Double] = ["design": 4.5]

An array preserves order and may contain duplicates. A set represents unique values without promising a meaningful order. A dictionary maps keys to values and is useful when the keys are dynamic.

API models often contain arrays of nested objects:

struct Order: Codable {
    let id: Int
    let items: [OrderItem]
}

struct OrderItem: Codable {
    let productID: Int
    let quantity: Int
}

An enumeration represents a restricted set of alternatives. A raw-value enumeration is useful when an API sends a known textual status:

enum OrderStatus: String, Codable {
    case pending
    case paid
    case shipped
    case cancelled
}

If the API may add statuses, synthesized decoding of this enum will fail for an unknown value. That may be appropriate for a strictly controlled contract, but a resilient client can include an unknown case through custom decoding or decode the raw string first. A practical approach is:

enum SafeOrderStatus {
    case pending, paid, shipped, cancelled
    case unknown(String)
}

This protects the application from server evolution while preserving the original value for logging or diagnostics.

5. Codable and JSON

Codable is a type alias combining Encodable and Decodable. Decodable converts external data into a Swift value; Encodable converts a Swift value into an external representation. JSON decoding is performed by JSONDecoder, and JSON encoding by JSONEncoder.

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)

let encoder = JSONEncoder()
let encodedData = try encoder.encode(user)

The try keyword acknowledges that decoding can fail. Typical failures include a missing required key, an incompatible type, invalid JSON syntax, and an incorrectly configured date strategy.

The synthesized Codable implementation works when property names and types correspond directly to the external data. Consider this JSON:

{
  "id": 7,
  "name": "Amina Khan",
  "email": "[email protected]"
}

It maps directly to the User structure. The synthesis is concise and reliable, so it should be preferred when the data contract is regular.

Understanding Decoding Errors

A production application should not reduce every failure to “something went wrong.” DecodingError provides diagnostic information:

do {
    let user = try decoder.decode(User.self, from: data)
    print(user)
} catch let error as DecodingError {
    switch error {
    case .keyNotFound(let key, let context):
        print("Missing key: \(key.stringValue), \(context.debugDescription)")
    case .typeMismatch(let type, let context):
        print("Type mismatch for \(type), \(context.codingPath)")
    case .valueNotFound(let type, let context):
        print("Missing value of type \(type), \(context.codingPath)")
    case .dataCorrupted(let context):
        print("Corrupt data: \(context.debugDescription)")
    @unknown default:
        print("Unknown decoding error")
    }
} catch {
    print("Unexpected error: \(error)")
}

In a user-facing application, these details would normally be logged for developers while the user receives a clear, nontechnical message.

6. Custom Coding Keys

External APIs often use naming conventions that differ from Swift. For example, JSON may use user_id, while Swift style convention uses userID. A nested CodingKeys enumeration maps the names:

struct User: Codable {
    let userID: Int
    let displayName: String

    enum CodingKeys: String, CodingKey {
        case userID = "user_id"
        case displayName = "display_name"
    }
}

Keys can also map reserved or inconvenient names:

struct SearchResult: Codable {
    let identifier: String
    let resultType: String

    enum CodingKeys: String, CodingKey {
        case identifier = "id"
        case resultType = "type"
    }
}

For consistent snake-case APIs, a decoder strategy can reduce repeated mappings:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

With this strategy, created_at can map to createdAt. Explicit CodingKeys remain preferable when a mapping is exceptional, when precision is important, or when the API naming does not follow a consistent rule. Do not combine a global strategy and explicit mappings without understanding their interaction.

7. Dates and Time Zones

Dates are a frequent source of defects because a date string is not self-describing. The client must know its format, precision, locale assumptions, and time-zone convention. ISO 8601 is a widely used interoperable format, for example 2025-04-12T14:30:00Z, where Z indicates UTC.

For ISO 8601 data:

struct Event: Codable {
    let title: String
    let startsAt: Date
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

If the server sends a custom format, use a DateFormatter or a modern format style appropriate to the deployment target. A formatter must have a fixed locale for machine-readable data, commonly en_US_POSIX, and the correct time zone:

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)

The date format must match the payload exactly. A formatter designed for 2025-04-12 cannot safely decode a timestamp containing hours and an offset. Dates should generally be stored and transmitted in UTC, then converted for display according to the user’s locale and time zone. A Date represents an instant; a calendar date such as a birthday may instead require a separate year-month-day model because time-zone conversion can change the displayed day.

8. Nested JSON and Custom Transformation

Nested JSON is modeled through nested Swift properties:

{
  "id": 42,
  "customer": {
    "name": "Luca",
    "address": {
      "city": "Rome",
      "country": "Italy"
    }
  },
  "lines": [
    {"sku": "A1", "quantity": 2}
  ]
}

Corresponding models are:

struct Invoice: Codable {
    let id: Int
    let customer: Customer
    let lines: [InvoiceLine]
}

struct Customer: Codable {
    let name: String
    let address: Address
}

struct Address: Codable {
    let city: String
    let country: String
}

struct InvoiceLine: Codable {
    let sku: String
    let quantity: Int
}

When the external structure does not match the desired Swift structure, implement init(from:) and, if necessary, encode(to:). For example, an API may return a price as a string:

struct Product: Decodable {
    let id: Int
    let price: Decimal

    enum CodingKeys: String, CodingKey {
        case id
        case price
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        let priceText = try container.decode(String.self, forKey: .price)
        guard let parsedPrice = Decimal(string: priceText,
                                        locale: Locale(identifier: "en_US_POSIX")) else {
            throw DecodingError.dataCorruptedError(
                forKey: .price,
                in: container,
                debugDescription: "Price is not a valid decimal string"
            )
        }
        price = parsedPrice
    }
}

For monetary values, Decimal is often preferable to Double because binary floating-point representation can introduce small rounding errors. Currency calculations also require a defined currency, scale, and rounding policy; a numeric type alone does not provide those business rules.

9. Safe Data Transformation

Decoding is only the first stage of safe data handling. A payload may be syntactically valid but semantically unacceptable: a negative quantity, an impossible rating, an empty identifier, or an unsupported currency. Validation should occur before data reaches critical application logic.

A useful architecture separates stages:

Stages from external JSON to a validated application model JSON response untrusted input Decode transport model Validate business rules App model
Stages from external JSON to a validated application model
Figure: Stages from external JSON to a validated application model

Figure: A defensive pipeline for transforming external data into application data.

For example:

struct ProductResponse: Decodable {
    let id: Int
    let name: String
    let price: Decimal
}

struct ProductViewModel {
    let title: String
    let formattedPrice: String
}

enum ProductMappingError: Error {
    case invalidIdentifier
    case invalidName
    case invalidPrice
}

func makeViewModel(from response: ProductResponse) throws -> ProductViewModel {
    guard response.id > 0 else { throw ProductMappingError.invalidIdentifier }
    guard !response.name.trimmingCharacters(in: .whitespaces).isEmpty else {
        throw ProductMappingError.invalidName
    }
    guard response.price >= 0 else { throw ProductMappingError.invalidPrice }

    return ProductViewModel(
        title: response.name,
        formattedPrice: "$\(response.price)"
    )
}

The example illustrates a key principle: transport decoding confirms shape and types; validation confirms domain rules; transformation prepares data for a particular user interface.

10. Applied Case: A Resilient Orders Screen

Background

Assume an illustrative retail application receives an order payload. The API uses snake-case keys, sends ISO 8601 timestamps, represents a missing delivery note as null, and may introduce new order statuses in future releases.

Problem

A direct model must display the order number, customer name, item count, order date, and status. The application should not crash if the note is missing or if a new status is introduced. It must also reject malformed quantities.

Approach

struct OrderResponse: Decodable {
    let orderID: Int
    let customer: CustomerResponse
    let placedAt: Date
    let status: String
    let deliveryNote: String?
    let items: [ItemResponse]

    enum CodingKeys: String, CodingKey {
        case orderID = "order_id"
        case customer
        case placedAt = "placed_at"
        case status
        case deliveryNote = "delivery_note"
        case items
    }
}

struct CustomerResponse: Decodable {
    let name: String
}

struct ItemResponse: Decodable {
    let quantity: Int
}

enum DisplayOrderStatus {
    case pending, paid, shipped, cancelled, unknown
}

struct OrderScreenModel {
    let heading: String
    let itemCount: Int
    let status: DisplayOrderStatus
    let note: String
}

The decoder is configured explicitly:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

Transformation can then classify status and calculate the total quantity safely:

func makeOrderScreenModel(from order: OrderResponse) throws -> OrderScreenModel {
    guard order.orderID > 0 else { throw ProductMappingError.invalidIdentifier }
    guard order.items.allSatisfy({ $0.quantity > 0 }) else {
        throw ProductMappingError.invalidPrice // replace with a dedicated quantity error in production
    }

    let displayStatus: DisplayOrderStatus
    switch order.status {
    case "pending": displayStatus = .pending
    case "paid": displayStatus = .paid
    case "shipped": displayStatus = .shipped
    case "cancelled": displayStatus = .cancelled
    default: displayStatus = .unknown
    }

    return OrderScreenModel(
        heading: "Order #\(order.orderID) — \(order.customer.name)",
        itemCount: order.items.reduce(0) { $0 + $1.quantity },
        status: displayStatus,
        note: order.deliveryNote ?? "No delivery note"
    )
}

In production, the deliberately reused error case should be replaced with a specific invalidQuantity case. This detail illustrates why expressive error types matter: accurate diagnostics support debugging, monitoring, and appropriate user feedback.

Outcome and Learning Points

The screen model is independent of API naming. Missing notes are handled intentionally, dates are interpreted according to the server contract, unknown statuses do not crash the interface, and invalid quantities are rejected before display. The application has also created a boundary where API changes can be absorbed without spreading server-specific details throughout the user interface.

11. Common Problems and Best Practices

Overusing optionals. Optionality should communicate meaningful uncertainty. Validate required values early and convert them into nonoptional domain properties when possible.

Using Double for money. Prefer Decimal or integer minor units, such as cents, with explicit currency and rounding rules.

Ignoring dates. Establish the server’s date format and time-zone policy. Test timestamps around daylight-saving transitions and midnight boundaries.

Assuming unknown enum values cannot occur. APIs evolve. Use an unknown representation when forward compatibility is important.

Decoding directly into view code. Keep networking, decoding, validation, and presentation responsibilities separate. This improves testing and reduces coupling.

Silently supplying defaults. A default can make an interface appear valid while hiding a server defect. Use defaults only when their meaning is defined.

Logging sensitive payloads. API data may contain names, addresses, tokens, or health information. Logs should be minimized, protected, and redacted. Never place authentication tokens or personal data in diagnostic output unnecessarily.

Trusting external input. Codable checks types, not all business rules. Validate ranges, identifiers, permissions, and relationships before use.

Testing only successful decoding. Unit tests should cover missing keys, null values, malformed dates, unknown enum cases, incorrect types, empty arrays, and boundary values.

12. Contemporary Practice and Governance

Swift concurrency commonly places decoding in asynchronous networking layers, where a response is decoded after an URLSession request and then passed across clearly defined boundaries. Models crossing concurrency boundaries should be designed with value semantics where practical, reducing shared-state risks.

API schema tools and generated clients can reduce manual model maintenance, but generated code does not remove the need for validation. A schema may describe types while omitting business constraints. Versioned endpoints, contract tests, and representative fixture data help detect breaking changes early.

Privacy and security must influence model design. Store only the fields required by the application, avoid persisting credentials in ordinary model objects, protect sensitive local storage, and follow applicable privacy requirements. Codable is a serialization mechanism, not an encryption or authentication mechanism. A decoded object should never be treated as proof that a user is authorized to perform an action.

Chapter Summary

Swift data modeling expresses application assumptions through structures, classes, optionals, collections, and enumerations. Structures are usually suitable for independent API values, while classes are appropriate when identity or shared mutable state matters. Optionals should represent genuine absence rather than conceal uncertainty.

Codable provides type-safe encoding and decoding for JSON. Synthesized conformance is effective for regular payloads, while custom coding keys handle naming differences and custom initializers handle irregular structures. Date decoding requires an explicit strategy that matches the server’s format and time-zone convention. Nested JSON maps naturally to nested Swift models.

Safe applications separate decoding from validation and transformation. Transport models can mirror external contracts, while domain and view models represent validated application needs. Robust implementations diagnose decoding failures, anticipate unknown enum values, test malformed data, protect sensitive information, and treat external payloads as untrusted input.

Key Terms

Data model: A typed representation of information and its relationships.

Transport model: A Swift type designed to mirror an external API payload.

Domain model: A type representing validated concepts required by application logic.

Structure: A Swift value type defined with struct.

Class: A Swift reference type defined with class.

Value semantics: Behavior in which copied values are independent.

Reference semantics: Behavior in which copied variables refer to the same instance.

Optional: A type that contains either a value or nil.

Collection: A type that stores multiple values, such as an array, set, or dictionary.

Enumeration: A type representing a finite set of alternatives.

Codable: The combined Encodable and Decodable protocols.

Encoding: Converting a Swift value into an external representation.

Decoding: Converting external data into a Swift value.

Coding key: A mapping between a Swift property and an external data key.

JSONDecoder: The Foundation type that decodes JSON into Swift values.

Date decoding strategy: The rule used to interpret external date representations.

Validation: Checking decoded data against domain rules.

Transformation: Converting one model representation into another.

Forward compatibility: The ability to continue operating when a service adds supported-but-unrecognized data.

Review Questions

5 Short Answer Questions

  1. What is the principal difference between a Swift structure and a class?
  2. Why should an API model use an optional property?
  3. What roles do Encodable and Decodable play in Codable?
  4. When is a custom CodingKeys enumeration required?
  5. Why must a JSON decoder be configured with an appropriate date strategy?

5 Long Answer Questions

  1. Explain how structures, classes, optionals, collections, and enumerations contribute to a robust data model.
  2. Describe the decoding process with JSONDecoder, including at least three causes of decoding failure.
  3. Compare synthesized Codable conformance with a custom init(from:) implementation.
  4. Explain why transport models and domain models may be separated in a production application.
  5. Discuss privacy, security, and governance considerations when decoding API responses.

3 Analytical / Application Questions

  1. Design Swift models for a nested JSON response containing a customer, an array of products, an optional discount code, and a snake-case creation timestamp. Explain your coding-key and date decisions.
  2. An API adds a new order status that is unknown to the current app. Compare a strict enum-decoding strategy with an unknown-case strategy and recommend one for a consumer application.
  3. A server returns product prices as strings, sometimes omits quantities, and occasionally sends negative values. Propose a decoding and validation pipeline that prevents invalid products from reaching the user interface.

Multiple Choice Questions with Answers

Question 1

Which statement best describes a Swift structure?

A. It is always mutable and globally shared.

B. It is a value type whose copied values are independent.

C. It must support inheritance.

D. It can only contain static properties.

Correct Answer: B. It is a value type whose copied values are independent.

Question 2

What does an optional property such as String? represent?

A. A string that is guaranteed to be nonempty.

B. A string that can never be decoded.

C. Either a String value or nil.

D. A string stored as a number.

Correct Answer: C. Either a String value or nil.

Question 3

Which protocol is responsible for converting external data into a Swift value?

A. Decodable

B. Encodable

C. Equatable

D. Hashable

Correct Answer: A. Decodable

Question 4

What is the purpose of a nested CodingKeys enumeration?

A. To make a class inherit from a structure.

B. To map Swift properties to external data keys.

C. To encrypt JSON values.

D. To make all properties optional.

Correct Answer: B. To map Swift properties to external data keys.

Question 5

Which decoder configuration is appropriate for a standard ISO 8601 date string?

A. decoder.dateDecodingStrategy = .iso8601

B. decoder.keyDecodingStrategy = .useDefaultKeys

C. decoder.dataDecodingStrategy = .base64

D. decoder.nonConformingFloatDecodingStrategy = .throw

Correct Answer: A. decoder.dateDecodingStrategy = .iso8601

Question 6

Why might Decimal be preferred to Double for monetary values?

A. Decimal automatically authenticates payments.

B. Decimal prevents all invalid API data.

C. Decimal is designed to avoid common binary floating-point representation issues.

D. Decimal can only store whole numbers.

Correct Answer: C. Decimal is designed to avoid common binary floating-point representation issues.

Question 7

What is the primary purpose of validation after decoding?

A. To convert JSON into XML.

B. To check business and domain rules beyond basic data shape.

C. To remove all optional properties.

D. To make every class final.

Correct Answer: B. To check business and domain rules beyond basic data shape.

Question 8

What may happen when synthesized decoding encounters an unknown raw-value enum case?

A. It always converts the value to nil.

B. It automatically creates a new enum case.

C. Decoding can fail.

D. It encrypts the unknown value.

Correct Answer: C. Decoding can fail.

Question 9

Which collection preserves order and may contain duplicate values?

A. Array

B. Set

C. Dictionary key set

D. Optional

Correct Answer: A. Array

Question 10

Why should sensitive API payloads generally be redacted in logs?

A. Sensitive values cannot be decoded.

B. Logs may expose personal information or credentials.

C. Redaction improves JSON syntax.

D. Redaction is required to create structures.

Correct Answer: B. Logs may expose personal information or credentials.

APIs & Data Management
Chapter 3 of 14