We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/dagba/ios-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
---
name: swift-codable-json
description: Use when implementing JSON encoding/decoding with Codable, handling API responses, encountering decoding errors, managing date formats, mapping snake_case to camelCase, or dealing with nested/inconsistent JSON structures
---
# Swift Codable for JSON Parsing
## Overview
**Codable provides type-safe JSON parsing but strict typing means any mismatch crashes decoding.** One date strategy per decoder, CodingKeys for every naming mismatch, custom decoders for nested structures.
**Core principle:** Design for API reality (not ideal JSON), fail gracefully with error handling, use optionals for unreliable data.
## Basic Patterns
### Pattern 1: Simple Mapping
```swift
// JSON: {"id": 123, "name": "Alice", "email": "alice@example.com"}
struct User: Codable {
let id: Int
let name: String
let email: String
}
// Usage:
let data = jsonString.data(using: .utf8)!
let user = try JSONDecoder().decode(User.self, from: data)
```
**Auto-synthesis works when:**
- Property names match JSON keys exactly
- All types match (String → String, Int → Int)
- All required properties present in JSON
### Pattern 2: CodingKeys for Name Mapping
**Problem:** API uses snake_case, Swift uses camelCase.
```swift
// JSON: {"user_id": 123, "first_name": "Alice", "created_at": "2026-01-15"}
struct User: Codable {
let userID: Int
let firstName: String
let createdAt: String
enum CodingKeys: String, CodingKey {
case userID = "user_id"
case firstName = "first_name"
case createdAt = "created_at"
}
}
```
**Rule:** Every property must appear in CodingKeys, even if name matches.
```swift
// ❌ WRONG: Compiler error (missing properties in CodingKeys)
enum CodingKeys: String, CodingKey {
case userID = "user_id" // Missing firstName and createdAt
}
// ✅ CORRECT: All properties listed
enum CodingKeys: String, CodingKey {
case userID = "user_id"
case firstName = "first_name"
case createdAt = "created_at"
}
```
## Date Handling
### Problem: Multiple Date Formats in Same Response
**CRITICAL:** JSONDecoder supports ONE date strategy at a time.
```swift
// JSON with mixed formats:
{
"created": "2026-01-15T10:30:00Z", // ISO8601
"published": "15/01/2026", // Custom format
"timestamp": 1705316400 // Unix timestamp
}
// ❌ WRONG: Can't set multiple strategies
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601 // Only applies to ONE field
```
### Solution 1: Custom Date Decoding
```swift
struct Article: Decodable {
let created: Date
let published: Date
let timestamp: Date
enum CodingKeys: String, CodingKey {
case created, published, timestamp
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// ISO8601 format
let iso8601Formatter = ISO8601DateFormatter()
let createdString = try container.decode(String.self, forKey: .created)
guard let createdDate = iso8601Formatter.date(from: createdString) else {
throw DecodingError.dataCorruptedError(forKey: .created, in: container, debugDescription: "Invalid ISO8601 date")
}
self.created = createdDate
// Custom format
let customFormatter = DateFormatter()
customFormatter.dateFormat = "dd/MM/yyyy"
let publishedString = try container.decode(String.self, forKey: .published)
guard let publishedDate = customFormatter.date(from: publishedString) else {
throw DecodingError.dataCorruptedError(forKey: .published, in: container, debugDescription: "Invalid date format")
}
self.published = publishedDate
// Unix timestamp
let timestampValue = try container.decode(TimeInterval.self, forKey: .timestamp)
self.timestamp = Date(timeIntervalSince1970: timestampValue)
}
}
```
### Solution 2: Dedicated Date Types
```swift
struct Article: Codable {
let created: String // Keep as String, parse when needed
let published: String
let timestamp: TimeInterval
var createdDate: Date? {
ISO8601DateFormatter().date(from: created)
}
var publishedDate: Date? {
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
return formatter.date(from: published)
}
var timestampDate: Date {
Date(timeIntervalSince1970: timestamp)
}
}
```
**Trade-off:** Less type-safe at decode time, but more flexible.
## Nested JSON Flattening
### Problem: Nested JSON Structure, Flat Swift Model
```swift
// API response:
{
"user": {
"id": 123,
"profile": {
"name": "Alice",
"avatar_url": "https://..."
}
},
"settings": {
"notifications": true
}
}
// Want: Flat Swift model
struct User {
let id: Int
let name: String
let avatarURL: String
let notifications: Bool
}
```
### Solution: Nested CodingKeys
```swift
struct User: Decodable {
let id: Int
let name: String
let avatarURL: String
let notifications: Bool
enum CodingKeys: String, CodingKey {
case user, settings
}
enum UserKeys: String, CodingKey {
case id, profile
}
enum ProfileKeys: String, CodingKey {
case name
case avatarURL = "avatar_url"
}
enum SettingsKeys: String, CodingKey {
case notifications
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Navigate to user.id
let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
id = try userContainer.decode(Int.self, forKey: .id)
// Navigate to user.profile.name and user.profile.avatar_url
let profileContainer = try userContainer.nestedContainer(keyedBy: ProfileKeys.self, forKey: .profile)
name = try profileContainer.decode(String.self, forKey: .name)
avatarURL = try profileContainer.decode(String.self, forKey: .avatarURL)
// Navigate to settings.notifications
let settingsContainer = try container.nestedContainer(keyedBy: SettingsKeys.self, forKey: .settings)
notifications = try settingsContainer.decode(Bool.self, forKey: .notifications)
}
}
```
## Optional vs Required Fields
### Pattern: Handle Unreliable Data
```swift
// API sometimes omits fields or sends null
// ❌ WRONG: Crashes when field missing
struct User: Codable {
let id: Int
let name: String
let email: String // Crashes if null or missing
}
// ✅ CORRECT: Optional for unreliable fields
struct User: Codable {
let id: Int
let name: String
let email: String? // nil if null or missing
}
// ✅ BETTER: Default values for missing fields
struct User: Codable {
let id: Int
let name: String
let email: String?
let isVerified: Bool
enum CodingKeys: String, CodingKey {
case id, name, email
case isVerified = "is_verified"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
email = try container.decodeIfPresent(String.self, forKey: .email)
isVerified = try container.decodeIfPresent(Bool.self, forKey: .isVerified) ?? false
}
}
```
**Rule:** Use `decodeIfPresent()` for optional fields, provide defaults where appropriate.
## Error Handling
### Pattern: Graceful Failure with Diagnostics
```swift
// ❌ WRONG: Silent failure or crash
let user = try! JSONDecoder().decode(User.self, from: data)
// ✅ CORRECT: Informative error handling
do {
let user = try JSONDecoder().decode(User.self, from: data)
return user
} catch let DecodingError.keyNotFound(key, context) {
print("Missing key: \(key.stringValue)")
print("Context: \(context.debugDescription)")
print("CodingPath: \(context.codingPath)")
return nil
} catch let DecodingError.typeMismatch(type, context) {
print("Type mismatch for type: \(type)")
print("Context: \(context.debugDescription)")
print("CodingPath: \(context.codingPath)")
return nil
} catch let DecodingError.valueNotFound(type, context) {
print("Value not found for type: \(type)")
print("Context: \(context.debugDescription)")
return nil
} catch {
print("Decoding error: \(error)")
return nil
}
```
**Production Pattern:**
```swift
enum NetworkError: LocalizedError {
case decodingFailed(reason: String)
var errorDescription: String? {
switch self {
case .decodingFailed(let reason):
return "Failed to decode response: \(reason)"
}
}
}
func decodeUser(from data: Data) throws -> User {
do {
return try JSONDecoder().decode(User.self, from: data)
} catch let DecodingError.keyNotFound(key, context) {
throw NetworkError.decodingFailed(
reason: "Missing key '\(key.stringValue)' at \(context.codingPath.map(\.stringValue).joined(separator: "."))"
)
} catch let DecodingError.typeMismatch(_, context) {
throw NetworkError.decodingFailed(
reason: "Type mismatch at \(context.codingPath.map(\.stringValue).joined(separator: "."))"
)
} catch {
throw NetworkError.decodingFailed(reason: error.localizedDescription)
}
}
```
## Performance Optimization
### Pattern: Background Decoding for Large JSON
```swift
// ❌ WRONG: Blocks main thread with 10MB JSON
let users = try JSONDecoder().decode([User].self, from: largeData)
updateUI(with: users)
// ✅ CORRECT: Background decoding
Task.detached {
let users = try JSONDecoder().decode([User].self, from: largeData)
await MainActor.run {
updateUI(with: users)
}
}
```
**Rule:** Decode > 1MB JSON on background thread.
### Pattern: Streaming for Very Large Files
```swift
// For multi-megabyte JSON files
func decodeInChunks(from fileURL: URL) throws -> [User] {
let stream = InputStream(url: fileURL)!
stream.open()
defer { stream.close() }
// Use JSONSerialization to read incrementally
var users: [User] = []
// Process in chunks to avoid loading entire file
return users
}
```
## Common Mistakes
| Mistake | Reality | Fix |
|---------|---------|-----|
| "All fields required" | APIs change, fields disappear. App crashes. | Use optionals for unreliable fields |
| "One CodingKeys entry per renamed field" | Must list ALL properties if using CodingKeys | List every property, even non-renamed |
| "decoder.dateDecodingStrategy handles all dates" | Only ONE strategy per decoder | Custom init(from:) for mixed formats |
| "try! is fine for trusted APIs" | APIs break. App crashes in production. | Always use do-catch with informative errors |
| "Type mismatch errors are obvious" | CodingPath can be nested 5 levels deep | Log context.codingPath for diagnosis |
| "String → Int will auto-convert" | Strict types. "123" ≠ 123 in Codable | Match API types exactly or use custom decoding |
## Quick Reference
**CodingKeys:**
```swift
enum CodingKeys: String, CodingKey {
case userID = "user_id" // Map names
case name // Keep same
}
```
**Nested containers:**
```swift
let outer = try decoder.container(keyedBy: OuterKeys.self)
let inner = try outer.nestedContainer(keyedBy: InnerKeys.self, forKey: .nested)
let value = try inner.decode(String.self, forKey: .value)
```
**Optional decoding:**
```swift
let value = try container.decodeIfPresent(String.self, forKey: .optional) ?? "default"
```
**Custom dates:**
```swift
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(String.self, forKey: .date)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
date = formatter.date(from: dateString)!
}
```
## Advanced Patterns
### Polymorphic Decoding
```swift
// JSON with type field:
{
"type": "image",
"url": "https://..."
}
// OR
{
"type": "video",
"duration": 120
}
enum Media: Decodable {
case image(url: String)
case video(duration: Int)
enum CodingKeys: String, CodingKey {
case type, url, duration
}
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 "image":
let url = try container.decode(String.self, forKey: .url)
self = .image(url: url)
case "video":
let duration = try container.decode(Int.self, forKey: .duration)
self = .video(duration: duration)
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown media type: \(type)"
)
}
}
}
```
### Lossy Array Decoding
**Problem:** Array with 1000 items, 3 are malformed. Want to decode 997, skip 3.
```swift
struct LossyArray<Element: Decodable>: Decodable {
let elements: [Element]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements: [Element] = []
while !container.isAtEnd {
do {
let element = try container.decode(Element.self)
elements.append(element)
} catch {
// Skip malformed element, continue
_ = try? container.decode(FailableDecodable.self)
}
}
self.elements = elements
}
}
private struct FailableDecodable: Decodable {}
// Usage:
let response = try JSONDecoder().decode(LossyArray<User>.self, from: data)
print("Decoded \(response.elements.count) valid users")
```
## Red Flags - STOP and Reconsider
- Using `try!` for API decoding → Add proper error handling
- All fields non-optional → Make unreliable fields optional
- Type mismatch error with no context → Log context.codingPath
- Single date strategy for mixed formats → Custom init(from:)
- Decoding multi-MB JSON on main thread → Background Task
- "keyNotFound" in production → Field is optional in API, make it optional in Swift
- CodingKeys missing properties → List ALL properties
## Real-World Impact
**Before:** App crashes for 5% of users when API adds nullable `middleName` field (strict non-optional String).
**After:** `var middleName: String?` Optional field. Zero crashes.
---
**Before:** 10-second freeze decoding 8MB user feed JSON on main thread.
**After:** Background Task decoding. UI responsive, feed loads in 2 seconds.
---
**Before:** "typeMismatch" error in logs with no details. Hours debugging.
**After:** Log `context.codingPath` → "items.3.metadata.tags". Found malformed tag in 4th item immediately.