Parse JSON in Swift

Swift parses JSON using JSONDecoder with the Codable protocol (a type alias for Decodable & Encodable). Annotating a struct or class with Codable automatically generates the encode/decode logic at compile time — no manual key mapping needed for matching property names. JSONDecoder().decode(User.self, from: data) decodes a Data value to a typed Swift struct in one line. Key differences from JavaScript: Swift is strict about types (IntDouble), JSON null maps to Swift Optional (nil), and missing keys require Optional or a default value. This guide covers Codable basics, CodingKeys for custom key names, optional and missing fields, date decoding strategies, error handling with DecodingError, and async API calls with URLSession.

Validate your JSON in Jsonic before parsing it in Swift.

Open JSON Formatter

Codable and JSONDecoder: basic parsing

Codable is a type alias for Decodable & Encodable. Use Decodable alone if you only need to parse — it compiles to slightly less code. Swift automatically synthesizes the decode implementation at compile time for any struct or class where every stored property is itself Codable.

import Foundation

// Define a struct conforming to Codable
struct User: Codable {
    var id: Int
    var name: String
    var email: String
    var age: Int
}

// JSON string -> Data -> decoded struct
let jsonString = """
{
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "age": 30
}
"""

let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()

do {
    let user = try decoder.decode(User.self, from: data)
    print(user.name)   // Alice
    print(user.email)  // alice@example.com
} catch {
    print("Decoding failed:", error)
}

// Decode an array of objects
let arrayJson = """
[
    {"id": 1, "name": "Alice", "email": "alice@example.com", "age": 30},
    {"id": 2, "name": "Bob",   "email": "bob@example.com",   "age": 25}
]
"""

let arrayData = arrayJson.data(using: .utf8)!
let users = try! decoder.decode([User].self, from: arrayData)
print(users.count)        // 2
print(users[0].name)      // Alice

// Encode back to JSON (JSONEncoder — the reverse of JSONDecoder)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try! encoder.encode(users[0])
print(String(data: encoded, encoding: .utf8)!)

The decode(_:from:) method throws on any failure — a missing required key, a type mismatch, or malformed JSON all produce a DecodingError. Always wrap calls in do { try ... } catch in production code; only use try! in tests or playgrounds where a crash is acceptable.

Swift typeJSON typeNotes
StringstringMust be quoted in JSON
Intinteger numberWill fail on floating-point JSON values
Double / FloatnumberAccepts both integer and floating-point JSON
Boolbooleantrue / false
Optional<T> (T?)value or nullnil if JSON value is null or key is absent
Array<T> ([T])arrayElements must all decode as type T
Dictionary<String, T>objectKeys must be String

CodingKeys: mapping JSON keys to Swift property names

By default, Swift requires property names to match JSON keys exactly (case-sensitive). When the API returns snake_case keys like user_id or created_at and you want camelCase Swift properties, you have two options: a CodingKeys enum for individual overrides, or keyDecodingStrategy = .convertFromSnakeCase for automatic bulk conversion.

import Foundation

// ── Approach 1: CodingKeys enum (fine-grained control) ────────────────────
struct Product: Codable {
    var productId: Int
    var productName: String
    var unitPrice: Double

    // Map each Swift property name to its JSON key string
    enum CodingKeys: String, CodingKey {
        case productId   = "product_id"
        case productName = "product_name"
        case unitPrice   = "unit_price"
    }
}

let json1 = """
{"product_id": 42, "product_name": "Widget", "unit_price": 9.99}
"""
let product = try! JSONDecoder().decode(Product.self, from: json1.data(using: .utf8)!)
print(product.productName) // Widget


// ── Approach 2: convertFromSnakeCase (automatic bulk conversion) ──────────
struct Order: Codable {
    var orderId: Int
    var customerName: String
    var totalAmount: Double
    var isPaid: Bool
}

let json2 = """
{
    "order_id": 100,
    "customer_name": "Alice",
    "total_amount": 49.95,
    "is_paid": true
}
"""
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let order = try! decoder.decode(Order.self, from: json2.data(using: .utf8)!)
print(order.customerName) // Alice
print(order.isPaid)       // true


// ── Approach 3: Exact match (default — no config needed) ──────────────────
struct Tag: Codable {
    var id: Int
    var name: String
    var color: String
}

let json3 = """{"id": 5, "name": "swift", "color": "orange"}"""
let tag = try! JSONDecoder().decode(Tag.self, from: json3.data(using: .utf8)!)
print(tag.name) // swift


// ── Tip: omit a property from encoding by not including it in CodingKeys ──
struct UserProfile: Codable {
    var id: Int
    var username: String
    var password: String  // will NOT be encoded/decoded — omit from CodingKeys

    enum CodingKeys: String, CodingKey {
        case id
        case username
        // password is intentionally omitted
    }
}

CodingKeys also controls which properties are included during encoding. If you omit a case from the enum, the corresponding property is excluded from both encoding and decoding — useful for local-only state like a computed cache or a sensitive field you never want serialized.

Optional fields and default values

Declare a property as T? (Optional) whenever the JSON key may be absent or its value may be null. JSONDecoder sets the property to nil in either case. Non-Optional properties throw DecodingError.keyNotFound when a key is missing and DecodingError.valueNotFound when the value is null.

import Foundation

struct Article: Codable {
    var id: Int            // required — throws if absent
    var title: String      // required — throws if absent
    var subtitle: String?  // optional — nil if absent or null
    var body: String       // required
    var tags: [String]?    // optional array — nil if absent
    var viewCount: Int?    // optional — nil if absent or null
}

// JSON with some fields missing
let json = """
{
    "id": 7,
    "title": "Getting started with Swift",
    "body": "Swift is a powerful language...",
    "view_count": null
}
"""

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

let article = try! decoder.decode(Article.self, from: json.data(using: .utf8)!)
print(article.title)                     // Getting started with Swift
print(article.subtitle ?? "(none)")      // (none)  — absent key → nil
print(article.viewCount ?? 0)            // 0        — null value → nil
print(article.tags?.count ?? 0)          // 0        — absent key → nil


// ── Default values via custom init(from:) ────────────────────────────────
// Swift default property values (var role = "user") are NOT respected by
// JSONDecoder when the key is present with a different value, but they ARE
// bypassed entirely — decodeIfPresent lets you supply a genuine fallback.

struct Config: Codable {
    var timeout: Int
    var retries: Int
    var verbose: Bool

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        timeout = try c.decodeIfPresent(Int.self,  forKey: .timeout)  ?? 30
        retries = try c.decodeIfPresent(Int.self,  forKey: .retries)  ?? 3
        verbose = try c.decodeIfPresent(Bool.self, forKey: .verbose)  ?? false
    }
}

let minimalJson = """{}"""
let config = try! JSONDecoder().decode(Config.self, from: minimalJson.data(using: .utf8)!)
print(config.timeout) // 30
print(config.retries) // 3

The cleanest pattern for optional fields is T? with a ?? defaultValue at the call site. Use a custom init(from decoder:) with decodeIfPresent only when you need a compile-time default that is embedded in the type itself rather than at every use site.

Decoding dates

JSON has no native date type — dates arrive as ISO 8601 strings or Unix timestamps. Set decoder.dateDecodingStrategy before calling decode and declare the property as Date; Swift handles the conversion automatically.

import Foundation

// ── Strategy 1: ISO 8601 string ("2026-05-11T12:00:00Z") ──────────────────
struct Event: Codable {
    var id: Int
    var name: String
    var startAt: Date
}

let isoJson = """
{"id": 1, "name": "WWDC", "start_at": "2026-06-09T17:00:00Z"}
"""

let isoDecoder = JSONDecoder()
isoDecoder.keyDecodingStrategy  = .convertFromSnakeCase
isoDecoder.dateDecodingStrategy = .iso8601

let event = try! isoDecoder.decode(Event.self, from: isoJson.data(using: .utf8)!)
print(event.startAt) // 2026-06-09 17:00:00 +0000


// ── Strategy 2: Unix timestamp in seconds (1715425200) ────────────────────
struct LogEntry: Codable {
    var message: String
    var timestamp: Date
}

let unixJson = """{"message": "server started", "timestamp": 1715425200}"""

let unixDecoder = JSONDecoder()
unixDecoder.dateDecodingStrategy = .secondsSince1970

let entry = try! unixDecoder.decode(LogEntry.self, from: unixJson.data(using: .utf8)!)
print(entry.timestamp) // 2024-05-11 13:00:00 +0000


// ── Strategy 3: Unix timestamp in milliseconds (1715425200000) ────────────
let msDecoder = JSONDecoder()
msDecoder.dateDecodingStrategy = .millisecondsSince1970


// ── Strategy 4: Custom DateFormatter for non-standard formats ─────────────
struct Report: Codable {
    var title: String
    var publishedAt: Date
}

let customJson = """{"title": "Q1 Report", "published_at": "2026-05-11 08:30:00"}"""

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

let customDecoder = JSONDecoder()
customDecoder.keyDecodingStrategy  = .convertFromSnakeCase
customDecoder.dateDecodingStrategy = .formatted(dateFormatter)

let report = try! customDecoder.decode(Report.self, from: customJson.data(using: .utf8)!)
print(report.publishedAt) // 2026-05-11 08:30:00 +0000

Note: .iso8601 only handles the exact format 2026-05-11T12:00:00Z (UTC with a literal Z suffix). For ISO 8601 dates with timezone offsets like +05:30, or fractional seconds like .000Z, use a custom DateFormatter with an ISO8601DateFormatter configured with the appropriate formatOptions, or use the .custom strategy.

Error handling with DecodingError

JSONDecoder.decode() throws DecodingError, which has four specific cases. Each case carries a context with a debugDescription and a codingPath array that shows exactly which key or index triggered the failure — invaluable for debugging nested structures.

import Foundation

struct Profile: Codable {
    var id: Int
    var username: String
    var score: Double
    var active: Bool
}

func decodeProfile(from jsonString: String) -> Profile? {
    guard let data = jsonString.data(using: .utf8) else {
        print("Could not convert string to Data")
        return nil
    }

    do {
        return try JSONDecoder().decode(Profile.self, from: data)
    } catch let error as DecodingError {
        switch error {
        case .typeMismatch(let type, let context):
            // A value existed but was the wrong type
            // e.g. JSON has "score": "high" but Swift expects Double
            print("Type mismatch: expected \(type)")
            print("Path: \(context.codingPath.map { $0.stringValue })")
            print("Detail: \(context.debugDescription)")

        case .valueNotFound(let type, let context):
            // Key exists but value is JSON null and property is non-Optional
            print("Value not found: expected \(type)")
            print("Path: \(context.codingPath.map { $0.stringValue })")
            print("Detail: \(context.debugDescription)")

        case .keyNotFound(let key, let context):
            // Required key is completely absent from the JSON
            print("Key not found: \(key.stringValue)")
            print("Path: \(context.codingPath.map { $0.stringValue })")
            print("Detail: \(context.debugDescription)")

        case .dataCorrupted(let context):
            // JSON itself is malformed (syntax error, invalid UTF-8, etc.)
            print("Data corrupted")
            print("Path: \(context.codingPath.map { $0.stringValue })")
            print("Detail: \(context.debugDescription)")

        @unknown default:
            print("Unknown DecodingError: \(error)")
        }
        return nil
    } catch {
        // Catch any other error (e.g., not a DecodingError at all)
        print("Unexpected error: \(error)")
        return nil
    }
}

// ── Examples ──────────────────────────────────────────────────────────────

// Success
let good = """{"id": 1, "username": "alice", "score": 98.5, "active": true}"""
let p1 = decodeProfile(from: good) // Profile(id: 1, ...)

// keyNotFound — "id" is absent
let missing = """{"username": "alice", "score": 98.5, "active": true}"""
let p2 = decodeProfile(from: missing)
// Key not found: id
// Path: []

// typeMismatch — score is a string, not a Double
let wrongType = """{"id": 1, "username": "alice", "score": "high", "active": true}"""
let p3 = decodeProfile(from: wrongType)
// Type mismatch: expected Double
// Path: ["score"]

// dataCorrupted — invalid JSON syntax
let broken = """{"id": 1, username: alice}"""
let p4 = decodeProfile(from: broken)
// Data corrupted

The codingPath array on each context contains the sequence of keys and array indices that led to the failure. For a deeply nested JSON object, it might read ["users", "0", "address", "zipCode"] — pointing you directly to the problematic field without manual investigation.

Fetching JSON from an API with URLSession and async/await

Swift 5.5 introduced structured concurrency with async/await. URLSession.data(from:) is the modern async API for HTTP requests — it suspends the current task while waiting for the network and resumes when the response arrives, with no callbacks or completion handlers required.

import Foundation

// ── Model ─────────────────────────────────────────────────────────────────
struct GithubUser: Decodable {
    var login: String
    var id: Int
    var name: String?
    var publicRepos: Int
    var followers: Int
}

// ── Generic fetch helper ──────────────────────────────────────────────────
func fetchJSON<T: Decodable>(
    url: URL,
    type: T.Type,
    decoder: JSONDecoder = JSONDecoder()
) async throws -> T {
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          (200..<300).contains(httpResponse.statusCode) else {
        throw URLError(.badServerResponse)
    }

    return try decoder.decode(T.self, from: data)
}

// ── Usage ─────────────────────────────────────────────────────────────────
func loadUser(login: String) async {
    guard let url = URL(string: "https://api.github.com/users/\(login)") else {
        print("Invalid URL")
        return
    }

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

    do {
        let user = try await fetchJSON(url: url, type: GithubUser.self, decoder: decoder)
        print("Login:", user.login)
        print("Name:", user.name ?? "(none)")
        print("Repos:", user.publicRepos)
        print("Followers:", user.followers)
    } catch let error as DecodingError {
        print("Decode error:", error)
    } catch {
        print("Network error:", error)
    }
}

// Call from an async context (e.g. a SwiftUI Task, or a unit test)
// Task { await loadUser(login: "apple") }


// ── POST request with JSON body ───────────────────────────────────────────
struct CreatePostRequest: Encodable {
    var title: String
    var body: String
    var userId: Int
}

struct Post: Decodable {
    var id: Int
    var title: String
}

func createPost(title: String, body: String) 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(
        CreatePostRequest(title: title, body: body, userId: 1)
    )

    let (data, _) = try await URLSession.shared.data(for: request)

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    return try decoder.decode(Post.self, from: data)
}

For more complex scenarios — custom headers, authentication, retry logic — create a dedicated URLSession with a URLSessionConfiguration instead of using URLSession.shared. Set timeoutIntervalForRequest and timeoutIntervalForResource to avoid indefinitely hanging requests in production apps.

Frequently asked questions

How do I parse JSON in Swift?

Conform your struct or class to Codable (or Decodable if you only need to read), then call JSONDecoder().decode(YourType.self, from: data). The data parameter is Data — convert a JSON string with string.data(using: .utf8)!. The decoder throws a DecodingError on failure, so wrap it in do { let result = try decoder.decode(…) } catch { print(error) }. Property names must match JSON keys exactly by default; use CodingKeys or keyDecodingStrategy = .convertFromSnakeCase to map different names.

What is the Codable protocol in Swift?

Codable is a type alias for Decodable & Encodable. A type conforming to Decodable can be decoded from JSON (or other formats) using JSONDecoder. A type conforming to Encodable can be encoded to JSON using JSONEncoder. Use Codable when you need both, Decodable when you only parse, Encodable when you only serialize. Swift automatically synthesizes the implementation if all stored properties are themselves Codable — no manual code needed for most structs and classes.

How do I handle optional JSON fields in Swift?

Mark the property as Optional: var email: String?. If the JSON key is absent or its value is null, the property will be nil. Non-Optional properties throw DecodingError.keyNotFound if the key is missing and DecodingError.valueNotFound if the value is null. To provide a default value, use Optional with a fallback at the call site: user.role ?? "viewer". For a compile-time default that survives decoding, implement a custom init(from decoder: Decoder) and use decodeIfPresent with a fallback.

How do I map snake_case JSON keys to camelCase Swift properties?

Set decoder.keyDecodingStrategy = .convertFromSnakeCase. This automatically maps user_iduserId, created_atcreatedAt, and any other snake_case key to its camelCase Swift equivalent. For fine-grained control over individual keys, use CodingKeys: add a nested enum CodingKeys: String, CodingKey to your struct where each case maps a Swift name to the exact JSON key string. Both approaches work together — convertFromSnakeCase applies to any key not overridden by CodingKeys. For the reverse (encoding), use encoder.keyEncodingStrategy = .convertToSnakeCase.

How do I decode dates from JSON in Swift?

Set decoder.dateDecodingStrategy before decoding. For ISO 8601 strings ("2026-05-11T12:00:00Z"): decoder.dateDecodingStrategy = .iso8601. For Unix timestamps in seconds (1715425200): .secondsSince1970. For Unix milliseconds (1715425200000): .millisecondsSince1970. For custom date formats: create a DateFormatter and use .formatted(formatter). With the correct strategy set, declare the property as var createdAt: Date in your Codable struct and it will decode automatically. See also the guide on JSON date formats for a broader overview.

What is the difference between JSONSerialization and JSONDecoder in Swift?

JSONSerialization (available since iOS 5) parses JSON into Any — you get a [String: Any] dictionary and manually cast each value with as?. It has no compile-time type safety and requires verbose optional chaining. JSONDecoder (iOS 8+) decodes JSON directly into typed Swift structs conforming to Codable — the compiler checks types, handles nesting automatically, and throws structured DecodingError with precise error paths. Use JSONDecoder for all new code. JSONSerialization is only useful when the JSON structure is fully dynamic and unknown at compile time — for example, a generic JSON diff tool or a schema-less document store. If you work across multiple platforms, see also parse JSON in Kotlin, parse JSON in Java, and parse JSON in C# for how other statically-typed languages approach the same problem.

Validate your JSON

Paste your JSON into Jsonic to catch syntax errors before parsing in Swift.

Open JSON Formatter