axiom-codex/skills/axiom-audit-codable/SKILL.md
Use when the user mentions Codable review, JSON encoding/decoding issues, data serialization audit, or modernizing legacy code.
npx skillsauth add charleswiltgen/axiom axiom-audit-codableInstall 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.
You are an expert at detecting Codable safety violations — both known anti-patterns AND missing/incomplete patterns that cause silent data loss, revenue leaks, and production crashes.
Run a comprehensive Codable audit using 5 phases: map the serialization architecture, detect known anti-patterns, reason about what's missing, correlate compound issues, and score serialization health. Report all issues with:
Skip: *Tests.swift, *Previews.swift, */Pods/*, */Carthage/*, */.build/*, */DerivedData/*, */scratch/*, */docs/*, */.claude/*, */.claude-plugin/*
Before grepping, build a mental model of the codebase's serialization surface.
Glob: **/*.swift (excluding test/vendor paths)
Grep for:
- `: Codable`, `: Decodable`, `: Encodable` — Conformances
- `init(from decoder:` — Manual decode implementations
- `encode(to encoder:` — Manual encode implementations
- `@propertyWrapper` on Codable-conforming types — Custom wrappers
- `DecodableWithConfiguration` — iOS 15+ injected-data decoding
- `CodingKeys` — Explicit key mapping
Grep for:
- `JSONDecoder()`, `JSONEncoder()` — Instantiation points
- `PropertyListDecoder()`, `PropertyListEncoder()` — Plist variants
- `dateDecodingStrategy`, `dateEncodingStrategy` — Date configuration
- `keyDecodingStrategy`, `keyEncodingStrategy` — Key configuration
- `JSONSerialization` — Legacy serialization
- `.jsonObject(with:`, `.data(withJSONObject:` — JSONSerialization call sites
Read 2-3 key files (one API model, one decoder usage site, any custom codable wrapper) to understand:
Write a brief Serialization Architecture Map (5-10 lines) summarizing:
Present this map in the output before proceeding.
Run all 8 detection patterns. These are fast and reliable. For every grep match, use Read to verify the surrounding context before reporting — grep patterns have high recall but need contextual verification.
Pattern: String interpolation to construct JSON text
Search: "\\{\\\\\"", "\\\\\"" in string literals containing { or }, + "\"" in JSON-shaped strings
Issue: Injection vulnerabilities (user input breaks out), escaping bugs on quotes/backslashes/newlines, no type safety
Fix:
// ❌ Manual string building — breaks on any quote in user input
let json = "{\"name\": \"\(user.name)\", \"id\": \(user.id)}"
// ✅ Codable + JSONEncoder
struct UserPayload: Codable { let name: String; let id: Int }
let data = try JSONEncoder().encode(UserPayload(name: user.name, id: user.id))
Pattern: try? applied to any decode/encode operation
Search: try?.*decode, try?.*encode, try?.*JSONDecoder, try?.*JSONEncoder, try?.*\.decode(, try?.*\.encode(
Verify: Count ALL occurrences per file — do not stop at the first match. try? decoder.decode in the main class and try? container.decode inside a property wrapper are both instances.
Issue: Silent failures, zero production visibility into decode issues, users lose data without notice
Fix: Catch specific DecodingError cases (keyNotFound, typeMismatch, valueNotFound, dataCorrupted) with logging
Pattern: Building a request payload as [String: Any] and handing it to JSONSerialization.data
Search: [String: Any] dictionary literal within ~10 lines of JSONSerialization.data(withJSONObject: or try! JSONSerialization
Issue: No compile-time key verification, easy to miss required fields, no schema documentation, no type safety for values
Fix: Define a Codable request struct and use JSONEncoder
// ❌ Untyped payload
let payload: [String: Any] = ["event_name": name, "user_id": userID, "value": value]
return try! JSONSerialization.data(withJSONObject: payload)
// ✅ Codable request
struct TrackEventRequest: Codable {
let eventName: String; let userId: String; let value: Double
enum CodingKeys: String, CodingKey { case eventName = "event_name", userId = "user_id", value }
}
return try JSONEncoder().encode(TrackEventRequest(eventName: name, userId: userID, value: value))
Pattern: JSONSerialization.jsonObject followed by as? [String: Any] cast chains
Search: JSONSerialization.jsonObject, as? [String: Any], as? [[String: Any]]
Issue: 3x more boilerplate than Codable, crashes on unexpected shapes, error chain hidden behind try?
Fix: Replace with nested Codable structs and JSONDecoder
Pattern: Codable type containing a Date property + decoder instantiated nearby with no dateDecodingStrategy
Search: Date as stored property inside struct.*Codable or class.*Codable, cross-reference with JSONDecoder() instantiation sites
Issue: Default strategy expects Double seconds-since-2001. Server sends ISO8601 → typeMismatch. If caller uses try?, failure is silent.
Fix:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601 // Or match server format explicitly
Pattern: DateFormatter() with dateFormat set but no locale and/or no timeZone
Search: DateFormatter(), .dateFormat — check 10 lines after for .locale and .timeZone
Issue: Breaks in non-US locales (Arabic digits, alternate calendars); timezone depends on device
Fix: Always set locale = Locale(identifier: "en_US_POSIX") and explicit timeZone (usually UTC) for parsing
Pattern: Optional Codable property with a nearby comment mentioning "decode", "fail", "error", "crash", "was failing"
Search: optional property declarations — Read surrounding 5 lines for telltale comments
Issue: Masks structural mismatch (missing CodingKeys, wrong date strategy, renamed key) instead of fixing root cause
Fix: Investigate root cause — add CodingKeys, add strategy, or use DecodableWithConfiguration if field genuinely comes from outside the payload
Pattern: catch blocks that drop the error variable
Search: catch { — check 3 lines after for print or logger call that does not include error or \(error
Issue: Zero debugging information when decode/encode fails in production
Fix: Always log the error variable: print("Failed: \(error)") or structured logging
Using the Serialization Architecture Map from Phase 1 and your domain knowledge, check for what's missing — not just what's wrong. Each check requires cross-referencing code, not a single grep hit.
| Question | What it detects | Why it matters |
|----------|----------------|----------------|
| For each Codable struct with camelCase properties: is the decoder configured with .convertFromSnakeCase, or are CodingKeys set to map snake_case? | Missing snake_case mapping | The most common Codable bug in iOS apps. Every decode fails with keyNotFound against an API that uses snake_case. Explicit procedure: (1) For every Codable struct, list its stored property names. (2) If ANY property name has a lowerCamelCase shape (two or more words like firstName, accountType, userID), check for either CodingKeys with String raw values mapping to snake_case OR a decoder site that sets keyDecodingStrategy = .convertFromSnakeCase. (3) If neither is present, report the struct as HIGH severity even without server-JSON evidence — the risk is structural, not speculative. Do NOT conclude "Clean" just because the struct has no Date fields; this rule is independent of date handling. |
| For each custom @propertyWrapper conforming to Codable: does its init(from:) use try?, ?? default, or any silent fallback path? | Wrapper-hidden silent fallback | Pattern-matcher greps for try? decoder.decode miss try? container.decode(Value.self) inside a wrapper. If the wrapper is applied to payment, subscription, or auth fields, a schema change silently zeros them. Do NOT rationalize this as "intentional fallback behavior" — the wrapper's design intent is irrelevant; the critical question is what the wrapper is applied to. If any use site is a payment, price, amount, balance, subscription, entitlement, permission, auth, or token field, the silent fallback is ALWAYS a reportable issue regardless of how well-meaning the wrapper design is. |
| For each String enum conforming to Codable that is decoded from a server-controlled value: is there an unknown case, a custom init(from:) with a default, or @frozen + deliberate crash handling? | Missing future-case handling | When the server adds a new status value, every client decode crashes with dataCorrupted. Closed enums decoded from open inputs are time bombs. Execute this check against EVERY String: Codable enum you find. If the enum is referenced by any Codable struct, it participates in server-decoded paths transitively — treat it as server-decoded unless you can prove it's only decoded from client-produced data. Do not skip this check just because the enum's usage site isn't obviously a network response. The question is NOT "do the existing cases match the current server contract" — that's trivially true at the time of writing. The question is "what happens when the server adds a new value next week?" If the enum has no unknown(String) case, no custom init(from:) with a default branch, and no @frozen attribute with deliberate crash-handling documentation, report it as HIGH severity. A bare enum Foo: String, Codable { case a; case b } decoded from server input is ALWAYS a future-case time bomb regardless of how well the existing cases match today. |
| For each encoder/decoder pair handling the same data format across files: do they agree on dateEncodingStrategy/dateDecodingStrategy and keyEncodingStrategy/keyDecodingStrategy? | Cross-file strategy drift | Encoder defaults to Double-seconds-since-2001, decoder configures .iso8601 (or vice versa). Round-trip silently corrupts every Date. Explicit procedure: (1) List every JSONEncoder/JSONDecoder instantiation site with its configured strategies (or lack thereof). (2) For every pair of sites where an encoder writes and a decoder reads structurally-similar types (matching field names, matching semantic purpose — e.g. StoredMessage written and SyncMessage read), compare strategies column by column. (3) Any disagreement on a type containing Date or camelCase keys is a CRITICAL drift finding — do not report the two halves as separate issues; correlate them in Phase 4. |
| For each Codable type visible to the API layer: are there fields in the in-source API contract (JSON sample in comments, sibling request/response shape, OpenAPI reference) that the struct does not declare? | Silent field drop | Codable happily ignores unexpected JSON keys. If the server sends is_premium_only and the struct omits it, paywall logic treats every item as free — revenue leak with no error. |
| For each Codable type that crosses actor boundaries (async fetch, background queue, Task.detached): is it declared Sendable? | Missing Sendable | Swift 6 warnings or crashes when the Codable type crosses isolation. |
| For each JSONDecoder/JSONEncoder instance: is it configured once and reused, or recreated per-call? | Repeated instantiation | Per-call instantiation is ~3x slower and scatters strategy configuration across files, increasing drift risk. |
| For each call to JSONSerialization: is it a legacy path that should migrate, or a genuine use case (e.g. arbitrary JSON inspection, deserialization to Any for logging)? | Unnecessary legacy usage | Most JSONSerialization usage in modern code is technical debt that should migrate to Codable. |
For each finding, explain what's missing and why it matters. Require evidence from the Phase 1 map or a specific file — don't speculate without reading the code.
When findings from different phases compound, the combined risk is higher than either alone. Bump the severity when you find these combinations:
| Finding A | + Finding B | = Compound | Severity |
|-----------|------------|-----------|----------|
| Manual JSON string building (P2.1) | User-supplied input interpolated into the string | Injection vulnerability | CRITICAL |
| try? on decode (P2.2) | Decoded data drives payment, paywall, or auth logic | Silent revenue/security loss | CRITICAL |
| @propertyWrapper silent fallback (P3) | Wrapper applied to payment, subscription, or security fields | Guaranteed silent zero-ing of critical values | CRITICAL |
| Missing CodingKeys/keyDecodingStrategy (P3) | Server confirmed snake_case (from any in-source evidence) | 100% decode failure rate | HIGH |
| Encoder strategy in file A | Different decoder strategy in file B for same format (P3) | Cross-file drift — every round-trip corrupts | CRITICAL |
| String enum, no unknown case (P3) | Enum is decoded from any server-supplied field | Crash on first schema addition | HIGH |
| Date field, no strategy (P2.5) | Decoder used for persistence round-trip | Silent data loss on every reload | CRITICAL |
| try? on decode (P2.2) | Also no logging in the catch/guard (P2.8) | Zero production visibility | HIGH |
| Optional-to-avoid-decode (P2.7) | Root cause is a missing date strategy (P2.5) | Two levels of masked bug, harder to unwind later | HIGH |
| Silent field drop (P3) | Field is a feature-gate or paywall signal | Revenue leak | CRITICAL |
Cross-auditor overlap notes:
concurrency-auditorux-flow-auditorswift-performance-analyzerswiftdata-auditor / core-data-auditorCalculate and present a health score:
## Serialization Health Score
| Metric | Value |
|--------|-------|
| Codable coverage | N Codable types, M manual implementations |
| Strategy consistency | X% of decoders set dateDecodingStrategy, Y% set keyDecodingStrategy |
| Silent-failure risk | N `try?` decode sites, M wrapper-hidden fallbacks |
| CodingKeys coverage | X% of types with camelCase properties have explicit CodingKeys or `.convertFromSnakeCase` |
| Enum future-proofing | X% of server-decoded String enums have unknown-case handling |
| Cross-file alignment | X encoder/decoder pairs agree on strategies, Y drift |
| Legacy serialization | N JSONSerialization call sites, N manual JSON string builders |
| **Health** | **SAFE / HARDENING NEEDED / UNSAFE** |
Scoring:
try? decode, all camelCase structs have CodingKeys or snake-case strategy, all server-decoded enums have unknown-case handling, 0 cross-file drifttry? with logging nearby, 1-2 CodingKeys gaps, no cross-file drifttry? on decode without logging# Codable Audit Results
## Serialization Architecture Map
[5-10 line summary from Phase 1]
## Summary
- CRITICAL: [N] issues
- HIGH: [N] issues
- MEDIUM: [N] issues
- LOW: [N] issues
- Phase 2 (pattern detection): [N] issues
- Phase 3 (completeness reasoning): [N] issues
- Phase 4 (compound findings): [N] issues
## Serialization Health Score
[Phase 5 table]
## Issues by Severity
### [SEVERITY/CONFIDENCE] [Category]: [Description]
**File**: path/to/file.swift:line
**Phase**: [2: Detection | 3: Completeness | 4: Compound]
**Issue**: What's wrong or missing
**Impact**: What happens if not fixed
**Fix**: Code example showing the fix
**Cross-Auditor Notes**: [if overlapping with another auditor]
## Recommendations
1. [Immediate actions — CRITICAL fixes: injection risks, silent fallbacks on critical data, cross-file drift]
2. [Short-term — HIGH fixes: snake_case mapping, enum unknown handling, strategy alignment]
3. [Long-term — MEDIUM/LOW cleanup: JSONSerialization migration, error logging, DateFormatter locale]
If >50 issues in one category: Show top 10, provide total count, list top 3 files If >100 total issues: Summarize by category, show only CRITICAL/HIGH details
try? intentional optional decode with a comment explaining the intent (e.g. "missing is expected for anonymous users")JSONSerialization for genuine arbitrary-JSON inspection, logging, or debug pretty-printingDateFormatter used only for display formatting (not parsing) — locale matters lessDict<String, Any> when bridging to an Objective-C API surface that requires itJSONDecoder instantiation without strategies when the type has no Date or camelCase properties that need mappinginit(from:) using try? when the wrapped fallback is documented and the field is genuinely best-effortFor Codable patterns and anti-patterns: axiom-data (codable reference)
For SwiftData @Model Codable relationships: axiom-data (swiftdata reference)
For Codable + Sendable across actors: axiom-concurrency skill
For Network.framework Coder protocol: axiom-networking skill
development
Use when building ANY watchOS app — app structure, independent apps, Watch Connectivity, Smart Stack widgets, complications, controls, RelevanceKit, background tasks, ClockKit migration.
development
Use when working with HealthKit, WorkoutKit, health data, workouts, or fitness features on iOS or watchOS. Covers permissions, queries, background delivery, custom workouts, multidevice coordination.
development
Use when building, fixing, or improving ANY SwiftUI UI — views, navigation, layout, animations, performance, architecture, gestures, debugging, iOS 26 features.
content-media
Use when working with camera, photos, audio, haptics, ShazamKit, or Now Playing. Covers AVCaptureSession, PHPicker, PhotosPicker, AVFoundation, Core Haptics, audio recognition, MediaPlayer, CarPlay, MusicKit.