Skip to content

From REST Response to Swift Codable on macOS

Updated

Turning a REST response into Swift models looks simple: copy the JSON keys, choose a few Swift types, and add Codable. Real API payloads make the job less mechanical. Keys use snake case, values may be missing or null, dates arrive as strings, and a nested object can invalidate an otherwise correct model.

This guide walks through a repeatable workflow for inspecting a JSON response, creating Swift Codable types, decoding the payload, and locating mismatches when decoding fails. You can follow it manually with Foundation or shorten the inspection and model-generation steps with a native Mac tool.

Turn a REST JSON response into Swift Codable models

  1. Inspect the response Read the actual payload before writing code: which values are integers rather than numeric strings, which can be null, which are nested objects, and how dates are formatted.
  2. Model the types Write one Codable struct per JSON object, giving nested objects their own focused type and marking genuinely nullable fields as optional.
  3. Configure the decoder Set keyDecodingStrategy to .convertFromSnakeCase and dateDecodingStrategy to .iso8601 so snake-case keys and timestamp strings map without hand-written CodingKeys.
  4. Check the HTTP response Verify the status code before decoding, so an HTML error page or JSON error envelope is not reported as a model-decoding failure.
  5. Decode and diagnose Decode the body, and on failure inspect the DecodingError coding path to identify the exact key, type, or value that did not match.
  6. Verify against real data Test the model against more than one representative payload and compare it with the documented API contract before committing it.

The example REST response

Assume a GET /users/42 endpoint returns this payload:

{
  "id": 42,
  "display_name": "Mina Patel",
  "email": "mina@example.com",
  "is_active": true,
  "created_at": "2026-07-28T09:41:12Z",
  "roles": ["admin", "editor"],
  "profile": {
    "avatar_url": "https://example.com/avatars/mina.png",
    "bio": null
  }
}

Before generating code, note what the payload actually guarantees:

  • id is an integer, not a numeric string.
  • roles is an array of strings.
  • profile is a nested object.
  • bio can be null, so its Swift property must be optional.
  • created_at appears to be an ISO 8601 timestamp.
  • The JSON keys use snake case while Swift conventionally uses camel case.

That short inspection prevents most first-pass decoding errors.

Create the Swift Codable models

The payload maps to two types:

import Foundation

struct User: Codable, Identifiable {
    let id: Int
    let displayName: String
    let email: String
    let isActive: Bool
    let createdAt: Date
    let roles: [String]
    let profile: Profile
}

struct Profile: Codable {
    let avatarUrl: URL?
    let bio: String?
}

Codable is a type alias for Encodable & Decodable. For a model whose stored properties are also codable, Swift can synthesize the required implementation. Apple describes this synthesis and the customization options in its guide to encoding and decoding custom types.

The optional properties deserve attention. A missing or null value can decode into String? or URL?. The same value will fail when the property is declared as a non-optional String or URL. Do not make every field optional as a blanket workaround; optionality should reflect the API contract.

Decode snake-case keys and ISO 8601 dates

The model uses displayName, but the server sends display_name. Configure JSONDecoder to bridge that naming difference and to parse the timestamp:

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

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

Apple documents convertFromSnakeCase as the strategy that converts snake-case JSON keys to camel-case Swift keys. It maps display_name to displayName, is_active to isActive, and avatar_url to avatarUrl.

If the server uses irregular names, define a CodingKeys enum instead of relying on a global conversion rule. Explicit keys are also useful when a property name should be clearer than the API field it represents.

Fetch and decode the response with URLSession

The complete request should verify the HTTP response before attempting to decode its body:

import Foundation

enum APIError: Error {
    case nonHTTPResponse
    case invalidStatusCode(Int)
}

func fetchUser(from url: URL) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse else {
        throw APIError.nonHTTPResponse
    }

    guard (200..<300).contains(httpResponse.statusCode) else {
        throw APIError.invalidStatusCode(httpResponse.statusCode)
    }

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

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

URLSession coordinates network data-transfer tasks, and its asynchronous data API returns both the received bytes and a response. Checking the status code first keeps an HTML error page or JSON error envelope from being reported as a misleading model-decoding failure.

Make decoding failures actionable

The default error description is often not enough. DecodingError includes a coding path that identifies the failing location. A small helper can turn that context into a useful diagnostic:

func describeDecodingError(_ error: Error) -> String {
    func path(_ codingPath: [CodingKey]) -> String {
        let value = codingPath.map(\.stringValue).joined(separator: ".")
        return value.isEmpty ? "<root>" : value
    }

    switch error {
    case let DecodingError.keyNotFound(key, context):
        return "Missing key '\(key.stringValue)' at \(path(context.codingPath))"

    case let DecodingError.typeMismatch(type, context):
        return "Expected \(type) at \(path(context.codingPath)): \(context.debugDescription)"

    case let DecodingError.valueNotFound(type, context):
        return "Missing \(type) value at \(path(context.codingPath)): \(context.debugDescription)"

    case let DecodingError.dataCorrupted(context):
        return "Invalid data at \(path(context.codingPath)): \(context.debugDescription)"

    default:
        return error.localizedDescription
    }
}

Use it where the request is called:

do {
    let url = URL(string: "https://api.example.com/users/42")!
    let user = try await fetchUser(from: url)
    print(user.displayName)
} catch {
    print(describeDecodingError(error))
}

Typical failures now become specific:

  • Missing key: The API omitted a required field. Confirm the contract or make the property optional when omission is valid.
  • Type mismatch: The model expects Int, but the response contains a quoted string such as "42".
  • Value not found: The response contains null for a non-optional property.
  • Data corrupted: A date string or another specially decoded value has an unsupported format.

Shorten the workflow on macOS

For a one-off payload, writing the structs manually is reasonable. Repeating the process across endpoints or languages is where a dedicated workflow saves time.

With JsonXmlEditor for macOS, the same process can stay in one local workspace:

  1. Create a request in the REST workspace and send it.
  2. Inspect the status, headers, and response body.
  3. Open the response as an editor document.
  4. Format and validate the JSON before generating code.
  5. Use JSONPath to inspect nested values when necessary.
  6. Generate a Swift DTO and review the inferred types and optional fields.
  7. Paste the model into Xcode and adapt naming or business rules to your project.

Generation should be the start of model design, not the final review. A tool can infer that a sample value is an integer, but it cannot know whether production sometimes returns null, whether a field is conditionally absent, or whether two payload variants should share one domain model. Compare generated code with the documented API contract and more than one real response.

A practical checklist

Before committing a generated or handwritten model, verify the following:

  • The top-level JSON shape matches the decoding target: object versus array.
  • Numeric strings are not accidentally modeled as numbers.
  • Nullable and conditionally absent fields are optional.
  • Nested objects have their own focused types.
  • Date decoding matches the server's exact format.
  • Snake-case conversion does not hide irregular field names.
  • HTTP errors are handled before JSON decoding.
  • The model has been tested against more than one representative payload.

Once those checks pass, converting an API response to Swift becomes a predictable process: inspect, model, configure, decode, and verify.

JsonXmlEditor is available free on the Mac App Store if you want to perform the REST, validation, query, and DTO-generation steps locally in one native Mac app.

Frequently asked questions

How do I decode snake_case JSON keys into camelCase Swift properties?
Set the decoder's keyDecodingStrategy to .convertFromSnakeCase. It maps display_name to displayName, is_active to isActive, and avatar_url to avatarUrl without hand-written CodingKeys. Use an explicit CodingKeys enum when the server uses irregular names.
How do I decode an ISO 8601 date string into a Swift Date?
Set the decoder's dateDecodingStrategy to .iso8601. If the server uses a different format, supply a custom formatter instead — a mismatched format surfaces as a DecodingError.dataCorrupted.
Which Swift properties should be optional?
Only the ones the API contract allows to be null or absent. A null value decodes fine into String? or URL? but fails against a non-optional String or URL. Making every field optional hides real contract changes instead of catching them.
Why does my decoding error not say which field failed?
The default description omits the location. DecodingError carries a coding path, so switching over keyNotFound, typeMismatch, valueNotFound, and dataCorrupted and printing that path tells you the exact key that mismatched.
Can I generate Swift models from a JSON response on Mac?
Yes. Json Xml Editor can send the REST request, format and validate the response, and generate a Swift DTO from it locally. Treat the generated code as a starting point and check it against the documented contract and more than one real payload.