Axiom axiom-audit-codable

Use when the user mentions Codable review, JSON encoding/decoding issues, data serialization audit, or modernizing legacy code.

install
source · Clone the upstream repo
git clone https://github.com/CharlesWiltgen/Axiom
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/CharlesWiltgen/Axiom "$T" && mkdir -p ~/.claude/skills && cp -r "$T/axiom-codex/skills/axiom-audit-codable" ~/.claude/skills/charleswiltgen-axiom-axiom-audit-codable && rm -rf "$T"
manifest: axiom-codex/skills/axiom-audit-codable/SKILL.md
source content

Codable Auditor Agent

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.

Your Mission

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:

  • File:line references
  • Severity/Confidence ratings (e.g., CRITICAL/HIGH, MEDIUM/LOW)
  • Fix recommendations with code examples

Files to Exclude

Skip:

*Tests.swift
,
*Previews.swift
,
*/Pods/*
,
*/Carthage/*
,
*/.build/*
,
*/DerivedData/*
,
*/scratch/*
,
*/docs/*
,
*/.claude/*
,
*/.claude-plugin/*

Phase 1: Map Serialization Architecture

Before grepping, build a mental model of the codebase's serialization surface.

Step 1: Inventory Codable Types

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

Step 2: Inventory Encoder/Decoder Sites

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

Step 3: Map Serialization Boundaries

Read 2-3 key files (one API model, one decoder usage site, any custom codable wrapper) to understand:

  • What Codable types cross which boundaries (network, disk, inter-process, pasteboard)
  • Which decoders/encoders are shared across files and which are one-offs
  • Whether date and key strategies are consistent per-boundary or drift between sites
  • Whether any types are encoded in one file and decoded in another (round-trip)

Output

Write a brief Serialization Architecture Map (5-10 lines) summarizing:

  • Codable type count and manual-implementation count
  • Decoder configuration patterns (which strategies are set, where, consistently or not)
  • Serialization boundaries (external API, local persistence, cache)
  • Custom wrappers present and their decode behavior (strict vs lenient)
  • Round-trip pairs (same data format produced by file A, consumed by file B)

Present this map in the output before proceeding.

Phase 2: Detect Known Anti-Patterns

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.

1. Manual JSON String Building (HIGH)

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))

2. try? Swallowing DecodingError (HIGH)

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

3. Dict-as-Payload Then JSONSerialization (MEDIUM)

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))

4. JSONSerialization + Cast Chain on Reads (MEDIUM)

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

5. Date Property Without Decoder Strategy (MEDIUM)

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

6. DateFormatter Without Locale/TimeZone (MEDIUM)

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

7. Optional-to-Avoid-Decode-Errors (MEDIUM)

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

8. Empty or Context-less Catch Blocks (LOW)

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

Phase 3: Reason About Serialization Completeness

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.

QuestionWhat it detectsWhy 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 mappingThe 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 fallbackPattern-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 handlingWhen 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 driftEncoder 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 dropCodable 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 SendableSwift 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 instantiationPer-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 usageMost
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.

Phase 4: Cross-Reference Findings

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= CompoundSeverity
Manual JSON string building (P2.1)User-supplied input interpolated into the stringInjection vulnerabilityCRITICAL
try?
on decode (P2.2)
Decoded data drives payment, paywall, or auth logicSilent revenue/security lossCRITICAL
@propertyWrapper silent fallback (P3)Wrapper applied to payment, subscription, or security fieldsGuaranteed silent zero-ing of critical valuesCRITICAL
Missing CodingKeys/keyDecodingStrategy (P3)Server confirmed snake_case (from any in-source evidence)100% decode failure rateHIGH
Encoder strategy in file ADifferent decoder strategy in file B for same format (P3)Cross-file drift — every round-trip corruptsCRITICAL
String enum, no unknown case (P3)Enum is decoded from any server-supplied fieldCrash on first schema additionHIGH
Date field, no strategy (P2.5)Decoder used for persistence round-tripSilent data loss on every reloadCRITICAL
try?
on decode (P2.2)
Also no logging in the catch/guard (P2.8)Zero production visibilityHIGH
Optional-to-avoid-decode (P2.7)Root cause is a missing date strategy (P2.5)Two levels of masked bug, harder to unwind laterHIGH
Silent field drop (P3)Field is a feature-gate or paywall signalRevenue leakCRITICAL

Cross-auditor overlap notes:

  • Codable + Sendable violations → compound with
    concurrency-auditor
  • Decode errors causing no UI feedback → compound with
    ux-flow-auditor
  • Repeated JSONDecoder instantiation in hot paths → compound with
    swift-performance-analyzer
  • @Model types with Codable relationships → compound with
    swiftdata-auditor
    /
    core-data-auditor

Phase 5: Serialization Health Score

Calculate 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:

  • SAFE: Explicit strategies on all decoders, 0 manual JSON building, 0
    try?
    decode, all camelCase structs have CodingKeys or snake-case strategy, all server-decoded enums have unknown-case handling, 0 cross-file drift
  • HARDENING NEEDED: Most decoders configured, rare
    try?
    with logging nearby, 1-2 CodingKeys gaps, no cross-file drift
  • UNSAFE: Manual JSON with user input, OR missing decoder strategies on persistence types, OR silent fallbacks on payment/auth data, OR cross-file strategy drift, OR
    try?
    on decode without logging

Output Format

# 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]

Output Limits

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

False Positives (Not Issues)

  • 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-printing
  • Manual JSON string literals in unit test fixtures
  • Optional properties that are optional per the API contract (documented, not masking a bug)
  • DateFormatter
    used only for display formatting (not parsing) — locale matters less
  • Dict<String, Any>
    when bridging to an Objective-C API surface that requires it
  • JSONDecoder
    instantiation without strategies when the type has no
    Date
    or camelCase properties that need mapping
  • Closed enum without unknown-case when the enum is decoded only from values the client itself produces (not server)
  • Custom
    init(from:)
    using
    try?
    when the wrapped fallback is documented and the field is genuinely best-effort

Related

For 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