Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

Swift Codable Mastery: Automating JSON-to-Struct

This technical guide provides an in-depth analysis of the json to swift struct engine, best practices for implementation, and data security standards.

JSON to Swift Struct: Codable, Custom Decoders, and Async/Await Network Calls

Swift's Codable protocol handles JSON mapping at compile time — #[derive(Deserialize)] in Rust, @JsonSerializable in Dart, but built directly into the Swift standard library with no dependencies. Converting your JSON to Swift structs with Codable gives you automatic JSON ↔ Swift conversion, type-safe enums for discriminated values, custom decoding strategies for dates and keys, and full integration with Swift's async/await networking. For SwiftUI apps, these structs drive the entire view state from decoded API responses.

Live Example: Order Model with Codable and CodingKeys

// Input JSON
{
  "order_id": "ORD-772",
  "status": "shipped",
  "customer_name": "Sarah Jenkins",
  "is_gift": false,
  "items": [
    { "sku": "WGT-001", "quantity": 2, "unit_price": 49.99 }
  ],
  "created_at": "2024-01-15T08:30:00Z"
}

// Generated Swift Structs
import Foundation

struct OrderItem: Codable {
    let sku:       String
    let quantity:  Int
    let unitPrice: Double

    enum CodingKeys: String, CodingKey {
        case sku
        case quantity
        case unitPrice = "unit_price"
    }
}

enum OrderStatus: String, Codable {
    case pending   = "pending"
    case shipped   = "shipped"
    case delivered = "delivered"
    case cancelled = "cancelled"
}

struct Order: Codable {
    let orderId:      String
    let status:       OrderStatus
    let customerName: String
    let isGift:       Bool
    let items:        [OrderItem]
    let createdAt:    Date

    enum CodingKeys: String, CodingKey {
        case orderId      = "order_id"
        case status
        case customerName = "customer_name"
        case isGift       = "is_gift"
        case items
        case createdAt    = "created_at"
    }
}

// Decoder with ISO 8601 date strategy
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

let order = try decoder.decode(Order.self, from: jsonData)

The CodingKeys enum maps snake_case JSON keys to camelCase Swift properties. For models where all keys follow snake_case → camelCase conversion, use decoder.keyDecodingStrategy = .convertFromSnakeCase instead — this applies the transformation globally without a per-property enum.

Automatic Key Conversion

struct UserProfile: Codable {
    let userId:      String
    let displayName: String
    let avatarUrl:   String?
    let createdAt:   Date
    let isVerified:  Bool
}

// With keyDecodingStrategy, no CodingKeys needed for snake_case JSON
let decoder = JSONDecoder()
decoder.keyDecodingStrategy    = .convertFromSnakeCase
decoder.dateDecodingStrategy   = .iso8601

// JSON: {"user_id": "...", "display_name": "...", "avatar_url": null, ...}
let profile = try decoder.decode(UserProfile.self, from: jsonData)

convertFromSnakeCase handles consecutive uppercase letters correctly: user_urluserUrl, html_contenthtmlContent. Use it when consuming REST APIs that consistently use snake_case keys.

Custom Decoding for Complex Transformations

struct Product: Decodable {
    let id:        String
    let name:      String
    let priceCents: Int      // JSON: "price_cents": 4999
    let price:     Double    // computed: 49.99

    enum CodingKeys: String, CodingKey {
        case id, name
        case priceCents = "price_cents"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id        = try container.decode(String.self, forKey: .id)
        name      = try container.decode(String.self, forKey: .name)
        priceCents = try container.decode(Int.self, forKey: .priceCents)
        price     = Double(priceCents) / 100.0  // derive from stored value
    }
}

// Flexible date decoding: try multiple formats
struct FlexibleEvent: Decodable {
    let id:        String
    let timestamp: Date

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

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)

        // Try ISO 8601 first, fall back to Unix timestamp
        if let isoString = try? container.decode(String.self, forKey: .timestamp) {
            guard let date = ISO8601DateFormatter().date(from: isoString) else {
                throw DecodingError.dataCorruptedError(forKey: .timestamp, in: container, debugDescription: "Invalid ISO8601 date")
            }
            timestamp = date
        } else {
            let epochSeconds = try container.decode(Double.self, forKey: .timestamp)
            timestamp = Date(timeIntervalSince1970: epochSeconds)
        }
    }
}

Async/Await Networking with URLSession

import Foundation

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

    func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
        let (data, response) = try await URLSession.shared.data(from: url)

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

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

// Usage in a SwiftUI ViewModel
@MainActor
class UserViewModel: ObservableObject {
    @Published var user: UserProfile?
    @Published var error: String?

    private let client = APIClient()

    func loadUser(id: String) async {
        do {
            let url = URL(string: "https://api.example.com/users/\(id)")!
            user = try await client.fetch(UserProfile.self, from: url)
        } catch {
            self.error = error.localizedDescription
        }
    }
}

// SwiftUI View
struct UserView: View {
    @StateObject private var viewModel = UserViewModel()
    let userId: String

    var body: some View {
        Group {
            if let user = viewModel.user {
                Text(user.displayName)
            } else if let error = viewModel.error {
                Text("Error: \(error)").foregroundColor(.red)
            } else {
                ProgressView()
            }
        }
        .task { await viewModel.loadUser(id: userId) }
    }
}

Discriminated Unions with Associated Values

// JSON polymorphism: {"type": "click", "x": 100, "y": 200}
enum DomEvent: Decodable {
    case click(x: Int, y: Int)
    case keypress(key: String, ctrl: Bool)
    case submit(formId: String)

    enum CodingKeys: String, CodingKey {
        case type, x, y, key, ctrl
        case formId = "form_id"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        switch type {
        case "click":
            let x = try container.decode(Int.self, forKey: .x)
            let y = try container.decode(Int.self, forKey: .y)
            self = .click(x: x, y: y)
        case "keypress":
            let key  = try container.decode(String.self, forKey: .key)
            let ctrl = try container.decode(Bool.self, forKey: .ctrl)
            self = .keypress(key: key, ctrl: ctrl)
        case "submit":
            let formId = try container.decode(String.self, forKey: .formId)
            self = .submit(formId: formId)
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type, in: container,
                debugDescription: "Unknown event type: \(type)"
            )
        }
    }
}

// Exhaustive handling in Swift
func handleEvent(_ event: DomEvent) {
    switch event {
    case .click(let x, let y):   print("Click at \(x), \(y)")
    case .keypress(let key, _):  print("Key: \(key)")
    case .submit(let formId):    print("Form: \(formId)")
    }
}

Best Practices for Production

  • Use structs, not classes, for JSON models: Swift structs are value types — they're copied, not referenced. This eliminates a whole class of bugs around shared mutable state in async code. Use classes only when you need reference semantics (shared mutable state, identity comparison).
  • Make all stored properties let: Immutable struct instances are thread-safe by default. When you need to update a value, create a new instance: var updated = original; updated = updated with the change isn't idiomatic — use a new initializer or var struct at the call site.
  • Handle unknown enum cases gracefully: enum Status: String, Codable throws on unknown values. For APIs that may add new statuses, add an unknown(String) case with custom decoding that captures the raw value instead of failing.
  • Reuse a single JSONDecoder instance: Creating a JSONDecoder on every request allocates a new instance. Create one statically or inject it as a dependency — the decoder is stateless and thread-safe after configuration.

FAQ

Q: What is Codable and why not just use [String: Any]?
A: Codable is a type alias for Encodable & Decodable. Unlike [String: Any] dictionaries, Codable structs give you compile-time field access, Swift's type system enforcing optionality, and IDE autocompletion. Dictionary access requires as? casts everywhere and silently passes nil when a key is mistyped.

Q: How do I handle arrays at the top level?
A: Decode directly to an array type: try decoder.decode([Order].self, from: jsonData). The decoder handles top-level arrays identically to arrays inside an object.

Q: Should I use Codable or a third-party library like Alamofire?
A: Use built-in Codable with URLSession async/await for new projects. Alamofire adds convenience but is a dependency; Swift's native async networking has largely eliminated the need for it in modern codebases. If you're already using Alamofire, its responseDecodable method integrates directly with any Codable type.

Q: How do I test JSON decoding?
A: Load test JSON from a file in your test bundle: Bundle(for: type(of: self)).url(forResource: "test-order", withExtension: "json"). Decode it with the same decoder configuration as production and assert on the decoded properties. This tests the actual mapping without network calls.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.