Learn  /  Guides

Fetching and decoding JSON in Swift with Codable and async/await

The clean, modern way to bring API data into an iPhone app, with no third-party libraries.

Guide9 min readUpdated 30 September 2026
SwiftiOS
All guides and cheat sheets

Most apps are windows onto data that lives somewhere else. Swift's Codable protocol and async/await concurrency make fetching and decoding that data concise and type-safe, with no third-party libraries needed.

From API to screen
APIJSON over HTTPS
URLSessionasync data(from:)
JSONDecoderCodable structs
SwiftUIList with .task

The JSON we want to decode

JSON
{
  "order_id": 1042,
  "customer_name": "Maria Rossi",
  "created_at": "2026-09-28T09:15:00Z",
  "total": 38.5,
  "items": [
    { "sku": "FW-01", "name": "Flat white", "qty": 2 },
    { "sku": "CR-02", "name": "Almond croissant", "qty": 1 }
  ]
}

Model it with Codable

Swift
import Foundation

struct Order: Codable, Identifiable {
    let orderId: Int
    let customerName: String
    let createdAt: Date
    let total: Decimal
    let items: [Item]

    var id: Int { orderId }

    struct Item: Codable, Hashable {
        let sku: String
        let name: String
        let qty: Int
    }
}

Properties use Swift naming; the decoder converts snake_case keys for you.

Configure the decoder

Swift
let decoder: JSONDecoder = {
    let d = JSONDecoder()
    d.keyDecodingStrategy = .convertFromSnakeCase
    d.dateDecodingStrategy = .iso8601
    return d
}()

Custom keys when names do not match

Swift
struct Product: Codable {
    let id: String
    let displayName: String
    let price: Decimal

    enum CodingKeys: String, CodingKey {
        case id = "sku"
        case displayName = "title"
        case price
    }
}

Fetch with async/await

Swift
enum APIError: Error {
    case badStatus(Int)
}

struct OrdersClient {
    let baseURL = URL(string: "https://api.example.com")!

    func recentOrders() async throws -> [Order] {
        let url = baseURL.appending(path: "orders/recent")
        let (data, response) = try await URLSession.shared.data(from: url)

        guard let http = response as? HTTPURLResponse else { return [] }
        guard (200..<300).contains(http.statusCode) else {
            throw APIError.badStatus(http.statusCode)
        }
        return try decoder.decode([Order].self, from: data)
    }
}

Show it in SwiftUI

Swift
import SwiftUI

struct OrdersView: View {
    @State private var orders: [Order] = []
    @State private var errorMessage: String?

    var body: some View {
        NavigationStack {
            List(orders) { order in
                VStack(alignment: .leading) {
                    Text(order.customerName).font(.headline)
                    Text(order.total, format: .currency(code: "GBP"))
                        .foregroundStyle(.secondary)
                }
            }
            .navigationTitle("Recent orders")
            .overlay {
                if let errorMessage {
                    ContentUnavailableView("Could not load orders",
                                           systemImage: "wifi.exclamationmark",
                                           description: Text(errorMessage))
                }
            }
            .task { await load() }
            .refreshable { await load() }
        }
    }

    private func load() async {
        do {
            orders = try await OrdersClient().recentOrders()
            errorMessage = nil
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}
.task
Starts the request when the view appears and cancels it automatically if the view disappears.
.refreshable
Adds pull-to-refresh for free.
ContentUnavailableView
A standard empty or error state, so failures look intentional.

.task starts the request when the view appears and cancels it automatically if the view disappears. .refreshable adds pull-to-refresh for free.

Debugging decoding errors

When decoding fails, print the full error. DecodingError tells you exactly which key or type did not match.

Swift
do {
    let order = try decoder.decode(Order.self, from: data)
    print(order)
} catch let DecodingError.keyNotFound(key, context) {
    print("Missing key:", key.stringValue, "at", context.codingPath.map(\.stringValue))
} catch let DecodingError.typeMismatch(type, context) {
    print("Type mismatch for", type, "at", context.codingPath.map(\.stringValue))
} catch {
    print(error)
}

Check your understanding

1/3Which decoder setting turns customer_name into customerName?

2/3Why use Decimal instead of Double for prices?

3/3What does SwiftUI's .task modifier do when the view disappears?

Keep the Swift cheat sheet nearby for syntax reminders, and see what is new in Swift 6.4 for the latest language changes.

Alessandro Ecclesie AgazziWritten by Alessandro Ecclesie Agazzi, analytics engineer in London. Updated 30 September 2026. About me

Found this useful?

New guides and "what's new" explainers on data and AI land here regularly. Follow along on LinkedIn, or share this one with a colleague.