Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Swift JSON Parsing and Formatting for iOS Development

For most iOS projects, the right default is simple: use Codable with JSONDecoder and JSONEncoder for typed app models, and reach for JSONSerialization only when the payload shape is unknown or you just need to reformat raw JSON. If you are starting from a minified API response, pretty-print it first so nested objects, nulls, and inconsistent keys are obvious before you write the model.

Start With The Best Default

Search visitors usually want the shortest path to working code. In current iOS development, that path is still Codable. It gives you type safety, fewer runtime casts, and cleaner compile-time failures when the payload changes.

  • Use Codable when the API has a known schema and you want Swift structs or enums.
  • Use JSONSerialization when the response is unstructured, has dynamic keys, or you are building a generic formatter/debugging tool.
  • Use pretty-printed JSON for logs, fixtures, snapshots, and debugging, not because servers need it.

Decode JSON From An API Response

Apple's async Foundation APIs make the common flow straightforward: build a request, fetch data with URLSession.shared.data(for:), then decode it with a configured decoder.

struct UserResponse: Codable {
    let id: Int
    let fullName: String
    let isActive: Bool
    let createdAt: Date
}

func fetchUser(id: Int) async throws -> UserResponse {
    var request = URLRequest(url: URL(string: "https://api.example.com/users/\(id)")!)
    request.setValue("application/json", forHTTPHeaderField: "Accept")

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

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

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

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

This example covers the real-world pieces the boilerplate examples often skip: HTTP status validation, snake_case conversion, and ISO 8601 date parsing. If your project still supports older OS versions, you can keep the same models and decoder setup while using the older completion-handler networking APIs instead of async/await.

Encode And Pretty-Print JSON

Formatting JSON for iOS work usually means one of two things: creating a request body or making JSON easy to read in logs and tests. JSONEncoder handles both.

struct UpdateUserRequest: Encodable {
    let fullName: String
    let isActive: Bool
    let tags: [String]
    let updatedAt: Date
}

let payload = UpdateUserRequest(
    fullName: "Alice Smith",
    isActive: true,
    tags: ["beta", "ios"],
    updatedAt: Date()
)

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]

let body = try encoder.encode(payload)
let jsonString = String(decoding: body, as: UTF8.self)

print(jsonString)

.prettyPrinted makes the output readable, .sortedKeys gives you deterministic output for tests and reviews, and .withoutEscapingSlashes keeps URLs readable in debug output.

Keys, Acronyms, Dates, And Missing Values

.convertFromSnakeCase is a good default, but it is not magic. Apple's documentation specifically notes that it cannot infer the capitalization of acronyms or initialisms. For example, a backend key like base_uri maps to baseUri, not baseURI.

struct FileRecord: Codable {
    let fileURL: URL
    let baseURI: URL
    let expiresAt: Date?

    private enum CodingKeys: String, CodingKey {
        case fileURL = "file_url"
        case baseURI = "base_uri"
        case expiresAt = "expires_at"
    }
}
  • Use explicit CodingKeys for acronym-heavy fields like userID, URL, or URI.
  • Use optionals only when the backend really allows missing or null values. Optional everything is easy, but it weakens your model.
  • Use ISO 8601 strategies when the API sends standards-based timestamps. If the server uses custom date strings, switch to a custom formatter or a custom decode implementation.

When JSONSerialization Is The Better Tool

JSONSerialization is not outdated. It is just a different tool. Use it when you do not want a fixed model type yet.

let object = try JSONSerialization.jsonObject(with: data)

if let dictionary = object as? [String: Any] {
    print(dictionary["debug_flag"] ?? "missing")
}

let prettyData = try JSONSerialization.data(
    withJSONObject: object,
    options: [.prettyPrinted, .sortedKeys]
)

let prettyJSON = String(decoding: prettyData, as: UTF8.self)
print(prettyJSON)
  • Good for dynamic keys, ad-hoc inspection, formatter utilities, and migration work.
  • Less good for production app models because it pushes type checking to runtime.
  • A practical rule: if you already know the shape of the payload, go back to Codable.

Debug Decoding Failures Fast

Most Swift JSON bugs are not parser bugs. They are contract bugs: wrong key, wrong type, wrong root object, or unexpectedly null data. Do not stop at catch { print(error) }. Inspect the actual DecodingError.

do {
    let user = try decoder.decode(UserResponse.self, from: data)
    print(user)
} catch let DecodingError.keyNotFound(key, context) {
    let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
    print("Missing key \(key.stringValue) at \(path)")
} catch let DecodingError.typeMismatch(type, context) {
    let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
    print("Type mismatch for \(type) at \(path)")
} catch let DecodingError.valueNotFound(type, context) {
    let path = context.codingPath.map { $0.stringValue }.joined(separator: ".")
    print("Missing value for \(type) at \(path)")
} catch {
    print("Unexpected error: \(error)")
}

That extra context usually tells you exactly what to fix: make a property optional, add a CodingKeys mapping, change the root decode type from User.self to [User].self, or write a custom initializer for inconsistent backend data.

Performance And Compatibility Notes

  • For typical mobile payloads, Foundation's JSON APIs are plenty fast. Large responses are where you start paying attention to memory and background work.
  • Avoid heavy decoding on the main actor if the payload is large enough to affect scrolling or first paint.
  • The async networking example on this page assumes URLSession async APIs, which are appropriate for modern iOS targets. Older targets can still use the same models with completion handlers.
  • If your backend sends inconsistent types like "42" sometimes and 42 other times, add a narrow custom decoder for that field instead of weakening your whole model.

Practical Takeaway

For a current Swift JSON parsing and formatting workflow on iOS, the best baseline is: pretty-print the raw payload, model it with Codable, decode with a configured JSONDecoder, encode with a configured JSONEncoder, and switch to JSONSerialization only when the structure is intentionally dynamic. That keeps the code Swifty, debuggable, and aligned with how current iOS apps actually talk to JSON APIs.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool