codable-patterns/SKILL.md
Encode and decode Swift types to and from JSON, property lists, and other external representations using Codable, JSONEncoder, and JSONDecoder. Use when implementing API response parsing, custom CodingKeys for key remapping, custom init(from:) or encode(to:) for complex transformations, nested or flattened JSON structures, heterogeneous array decoding, date and data decoding strategies, lossy array wrappers, Codable integration with URLSession, SwiftData, or UserDefaults, or when configuring encoder/decoder output formatting and key strategies.
npx skillsauth add abanoub-ashraf/manus-skills-import codable-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Encode and decode Swift types using Codable (Encodable & Decodable) with
JSONEncoder, JSONDecoder, and related APIs. Targets Swift 6.2 / iOS 26+.
When all stored properties are themselves Codable, the compiler synthesizes
conformance automatically:
struct User: Codable {
let id: Int
let name: String
let email: String
let isVerified: Bool
}
let user = try JSONDecoder().decode(User.self, from: jsonData)
let encoded = try JSONEncoder().encode(user)
Prefer Decodable for read-only API responses and Encodable for write-only.
Use Codable only when both directions are required.
Rename JSON keys without writing a custom decoder by declaring a CodingKeys
enum:
struct Product: Codable {
let id: Int
let displayName: String
let imageURL: URL
let priceInCents: Int
enum CodingKeys: String, CodingKey {
case id
case displayName = "display_name"
case imageURL = "image_url"
case priceInCents = "price_in_cents"
}
}
Every stored property must appear in the enum. Omitting a property from
CodingKeys excludes it from encoding/decoding -- provide a default value or
compute it separately.
Override init(from:) and encode(to:) for transformations the synthesized
conformance cannot handle:
struct Event: Codable {
let name: String
let timestamp: Date
let tags: [String]
enum CodingKeys: String, CodingKey {
case name, timestamp, tags
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
// Decode Unix timestamp as Double, convert to Date
let epoch = try container.decode(Double.self, forKey: .timestamp)
timestamp = Date(timeIntervalSince1970: epoch)
// Default to empty array when key is missing
tags = try container.decodeIfPresent([String].self, forKey: .tags) ?? []
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(timestamp.timeIntervalSince1970, forKey: .timestamp)
try container.encode(tags, forKey: .tags)
}
}
Use nestedContainer(keyedBy:forKey:) to navigate and flatten nested JSON:
// JSON: { "id": 1, "location": { "lat": 37.7749, "lng": -122.4194 } }
struct Place: Decodable {
let id: Int
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey { case id, location }
enum LocationKeys: String, CodingKey { case lat, lng }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
let location = try container.nestedContainer(
keyedBy: LocationKeys.self, forKey: .location)
latitude = try location.decode(Double.self, forKey: .lat)
longitude = try location.decode(Double.self, forKey: .lng)
}
}
Chain multiple nestedContainer calls to flatten deeply nested structures.
Also use nestedUnkeyedContainer(forKey:) for nested arrays.
Decode arrays of mixed types using a discriminator field:
// JSON: [{"type":"text","content":"Hello"},{"type":"image","url":"pic.jpg"}]
enum ContentBlock: Decodable {
case text(String)
case image(URL)
enum CodingKeys: String, CodingKey { case type, content, url }
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 "text":
let content = try container.decode(String.self, forKey: .content)
self = .text(content)
case "image":
let url = try container.decode(URL.self, forKey: .url)
self = .image(url)
default:
throw DecodingError.dataCorruptedError(
forKey: .type, in: container,
debugDescription: "Unknown type: \(type)")
}
}
}
let blocks = try JSONDecoder().decode([ContentBlock].self, from: jsonData)
Configure JSONDecoder.dateDecodingStrategy to match your API:
let decoder = JSONDecoder()
// ISO 8601 (e.g., "2024-03-15T10:30:00Z")
decoder.dateDecodingStrategy = .iso8601
// Unix timestamp in seconds (e.g., 1710499800)
decoder.dateDecodingStrategy = .secondsSince1970
// Custom DateFormatter
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(formatter)
// Custom closure for multiple formats
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
if let date = ISO8601DateFormatter().date(from: string) { return date }
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "Cannot decode date: \(string)")
}
Set the matching strategy on JSONEncoder:
encoder.dateEncodingStrategy = .iso8601
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64 // Base64-encoded Data fields
decoder.keyDecodingStrategy = .convertFromSnakeCase // snake_case -> camelCase
// {"user_name": "Alice"} maps to `var userName: String` -- no CodingKeys needed
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.keyEncodingStrategy = .convertToSnakeCase
By default, one invalid element fails the entire array. Use a wrapper to skip invalid elements:
struct LossyArray<Element: Decodable>: Decodable {
let elements: [Element]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements: [Element] = []
while !container.isAtEnd {
if let element = try? container.decode(Element.self) {
elements.append(element)
} else {
_ = try? container.decode(AnyCodableValue.self) // advance past bad element
}
}
self.elements = elements
}
}
private struct AnyCodableValue: Decodable {}
Wrap primitives for type safety using singleValueContainer():
struct UserID: Codable, Hashable {
let rawValue: String
init(_ rawValue: String) { self.rawValue = rawValue }
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
rawValue = try container.decode(String.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}
// JSON: "usr_abc123" decodes directly to UserID
Use decodeIfPresent with nil-coalescing to provide defaults:
struct Settings: Decodable {
let theme: String
let fontSize: Int
let notificationsEnabled: Bool
enum CodingKeys: String, CodingKey {
case theme, fontSize = "font_size"
case notificationsEnabled = "notifications_enabled"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
theme = try container.decodeIfPresent(String.self, forKey: .theme) ?? "system"
fontSize = try container.decodeIfPresent(Int.self, forKey: .fontSize) ?? 16
notificationsEnabled = try container.decodeIfPresent(
Bool.self, forKey: .notificationsEnabled) ?? true
}
}
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
// Non-conforming floats (NaN, Infinity are not valid JSON)
encoder.nonConformingFloatEncodingStrategy = .convertToString(
positiveInfinity: "Infinity", negativeInfinity: "-Infinity", nan: "NaN")
decoder.nonConformingFloatDecodingStrategy = .convertFromString(
positiveInfinity: "Infinity", negativeInfinity: "-Infinity", nan: "NaN")
let plistEncoder = PropertyListEncoder()
plistEncoder.outputFormat = .xml // or .binary
let data = try plistEncoder.encode(settings)
let decoded = try PropertyListDecoder().decode(Settings.self, from: data)
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw APIError.invalidResponse
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(User.self, from: data)
}
// Generic API envelope for wrapped responses
struct APIResponse<T: Decodable>: Decodable {
let data: T
let meta: Meta?
struct Meta: Decodable { let page: Int; let totalPages: Int }
}
let users = try decoder.decode(APIResponse<[User]>.self, from: data).data
Codable structs work as composite attributes in SwiftData models. In iOS 18+,
SwiftData natively supports them without explicit @Attribute(.transformable):
struct Address: Codable {
var street: String
var city: String
var zipCode: String
}
@Model class Contact {
var name: String
var address: Address? // Codable struct stored as composite attribute
init(name: String, address: Address? = nil) {
self.name = name; self.address = address
}
}
Store Codable values via RawRepresentable for @AppStorage:
struct UserPreferences: Codable {
var showOnboarding: Bool = true
var accentColor: String = "blue"
}
extension UserPreferences: RawRepresentable {
init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let decoded = try? JSONDecoder().decode(Self.self, from: data)
else { return nil }
self = decoded
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let string = String(data: data, encoding: .utf8)
else { return "{}" }
return string
}
}
struct SettingsView: View {
@AppStorage("userPrefs") private var prefs = UserPreferences()
var body: some View {
Toggle("Show Onboarding", isOn: $prefs.showOnboarding)
}
}
1. Not handling missing optional keys:
// DON'T -- crashes if key is absent
let value = try container.decode(String.self, forKey: .bio)
// DO -- returns nil for missing keys
let value = try container.decodeIfPresent(String.self, forKey: .bio) ?? ""
2. Failing entire array when one element is invalid:
// DON'T -- one bad element kills the whole decode
let items = try container.decode([Item].self, forKey: .items)
// DO -- use LossyArray or decode elements individually
let items = try container.decode(LossyArray<Item>.self, forKey: .items).elements
3. Date strategy mismatch:
// DON'T -- default strategy expects Double, but API sends ISO string
let decoder = JSONDecoder() // dateDecodingStrategy defaults to .deferredToDate
// DO -- set strategy to match your API format
decoder.dateDecodingStrategy = .iso8601
4. Force-unwrapping decoded optionals:
// DON'T
let user = try? decoder.decode(User.self, from: data)
print(user!.name)
// DO
guard let user = try? decoder.decode(User.self, from: data) else { return }
5. Using Codable when only Decodable is needed:
// DON'T -- unnecessarily constrains the type to also be Encodable
struct APIResponse: Codable { let id: Int; let message: String }
// DO -- use Decodable for read-only API responses
struct APIResponse: Decodable { let id: Int; let message: String }
6. Manual CodingKeys for simple snake_case APIs:
// DON'T -- verbose boilerplate for every model
enum CodingKeys: String, CodingKey {
case userName = "user_name"
case avatarUrl = "avatar_url"
}
// DO -- configure once on the decoder
decoder.keyDecodingStrategy = .convertFromSnakeCase
Decodable only when encoding is not neededdecodeIfPresent used with defaults for optional or missing keyskeyDecodingStrategy = .convertFromSnakeCase used instead of manual CodingKeys for simple snake_case APIsdateDecodingStrategy matches the API date formatinit(from:) validates and transforms data instead of post-decode fixupsJSONEncoder.outputFormatting includes .sortedKeys for deterministic test outputsingleValueContainer for clean JSONAPIResponse<T> wrapper used for consistent API envelope handling@AppStorage Codable types conform to RawRepresentableCodable structsdevelopment
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes