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, orURI. - 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
URLSessionasync 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 and42other 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