SafeDecoding

0.7.0

AltiAntonov/SafeDecoding

What's New

SafeDecoding 0.7.0

2026-05-04T19:19:50Z

SafeDecoding 0.7.0 hardens the package for 1.0.0 by validating API coherence, clarifying adoption guidance, and making the pre-stable surface more explicit.

Added

  • Clearer wrapper-selection guidance in the README, including when to stay strict.
  • Explicit documentation of stable fieldPath formatting expectations for object keys and array indexes.

Changed

  • Public documentation and doc comments now frame 0.7.0 as the pre-1.0 stabilization release.
  • Wrapper and report guidance is now aligned around explicit recovery boundaries rather than feature-by-feature discovery.

Breaking Changes

  • None. 0.7.0 is a stabilization release and is source-compatible with 0.6.0.

What's Changed

Full Changelog: 0.6.0...0.7.0

SafeDecoding

Resilient JSON decoding for Swift Decodable models when real-world payloads are messy.

Swift version compatibility Platform compatibility MIT License Swift workflow

Features · Installation · Quick Start · When To Use · Good Fits · Weaker Fits · Runtime Semantics · Documentation · Testing

Features

  • @SafeDecodable for optional field failure isolation
  • @LossySafeDecodable for opt-in lossy array recovery
  • SafeDecodingFallbackProvider for typed fallback values on required fields
  • @SafeFallbackDecodable for fallback-backed required-value decoding
  • SafeJSONDecoder for app-level JSON decode plus report capture
  • missing safe fields decode to nil
  • broken safe fields do not fail the whole model
  • broken fallback-backed fields emit placeholder diagnostics and use the provider value
  • placeholder diagnostics for decode issues shipped in 0.1.0 and carried through the released 0.3.x line

The current public API is intentionally centered on:

  • SafeDecodable
  • LossySafeDecodable
  • SafeDecodingFallbackProvider
  • SafeFallbackDecodable
  • SafeJSONDecoder
  • SafeDecodingReport
  • SafeDecodingDiagnostics
  • SafeDecodingIssue

Installation

dependencies: [
    .package(url: "https://github.com/AltiAntonov/SafeDecoding.git", from: "0.6.0")
]

Then add the product to your target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "SafeDecoding", package: "SafeDecoding")
    ]
)

Quick Start

import SafeDecoding

enum UnknownRoleFallback: SafeDecodingFallbackProvider {
    static let fallbackValue = "unknown"
}

struct User: Decodable {
    let id: Int
    @SafeDecodable var name: String?
    @SafeFallbackDecodable<UnknownRoleFallback> var role: String
}

If name is missing or malformed, decoding still succeeds and name becomes nil. If role is present but malformed, decoding still succeeds and role becomes "unknown".

For example, this dirty payload still decodes:

{
  "id": 7,
  "name": 404,
  "role": 42
}

name falls back to nil, while role falls back to the typed provider value.

Choosing The API

Use the wrapper that matches the recovery boundary you actually want:

  • @SafeDecodable Use when one optional-like value may be missing or malformed, and nil is the correct recovery.
  • @SafeFallbackDecodable Use when one required value must stay usable, and you want a typed fallback such as "unknown", "ZZ", or an enum case.
  • @LossySafeDecodable Use when one array field should preserve valid elements and skip malformed ones.
  • SafeJSONDecoder Use when you want the decoded value and SafeDecodingReport in one call.
  • Strict decoding Use plain values and plain arrays when malformed data should still fail the decode.

This is the intended 1.0 mental model: wrappers mark recovery boundaries explicitly in the model, while unwrapped values remain strict.

Nested Models

Nested wrapped fields recover the same way as top-level wrapped fields.

import SafeDecoding

enum UnknownRoleFallback: SafeDecodingFallbackProvider {
    static let fallbackValue = "unknown"
}

enum Visibility: String, Decodable {
    case `public`
    case `private`
}

struct Profile: Decodable {
    @SafeDecodable var name: String?
    @SafeFallbackDecodable<UnknownRoleFallback> var role: String
}

struct Preferences: Decodable {
    @SafeDecodable var visibility: Visibility?
}

struct User: Decodable {
    let id: Int
    let profile: Profile
    let preferences: Preferences
}

Dirty nested payload:

{
  "id": 7,
  "profile": {
    "name": 404,
    "role": 42
  },
  "preferences": {
    "visibility": "friends-only"
  }
}

Safe decode plus report:

let result = try SafeJSONDecoder().decode(User.self, from: data)

result.value.profile.name // nil
result.value.profile.role // "unknown"
result.value.preferences.visibility // nil

result.report.issues.map(\.fieldPath)
// ["profile.name", "profile.role", "preferences.visibility"]

Strict nested fields are still strict. If a nested property is not wrapped and its decode fails, SafeJSONDecoder rethrows the underlying DecodingError instead of reconstructing a partial model.

Lossy Arrays

Use @LossySafeDecodable when one specific array field should preserve valid elements instead of failing the whole payload because one entry is malformed.

import SafeDecoding

struct User: Decodable {
    let id: Int
    let name: String
}

struct Response: Decodable {
    @LossySafeDecodable var users: [User]
}

Dirty list payload:

{
  "users": [
    { "id": 1, "name": "Ava" },
    { "id": "oops", "name": "Broken" },
    { "id": 2, "name": "Noah" }
  ]
}

Safe decode plus report:

let result = try SafeJSONDecoder().decode(Response.self, from: data)

result.value.users
// [User(id: 1, name: "Ava"), User(id: 2, name: "Noah")]

result.report.issues.map(\.fieldPath)
// ["users.1.id"]

@LossySafeDecodable is field-scoped and opt-in. Plain [User] remains strict and still throws if any element is malformed.

SafeDecodingIssue.fieldPath is intended to stay stable for adopters: object keys are dot-joined, and array elements use numeric indexes such as users.1.id.

Safe JSON Decoder

Use SafeJSONDecoder as the app-level entry point when you want the decoded value and a structured report in one call.

let result = try SafeJSONDecoder().decode(User.self, from: data)

let user = result.value
let report = result.report

SafeJSONDecoder is a convenience wrapper around JSONDecoder and SafeDecodingDiagnostics.capture. It does not implement a custom decoder and it does not reconstruct partial models when standard Decodable initialization fails.

You can inject a configured JSONDecoder:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

let safeDecoder = SafeJSONDecoder(jsonDecoder: decoder)
let result = try safeDecoder.decode(User.self, from: data)

Reports

Use SafeDecodingDiagnostics.capture when you want structured issue inspection around an existing decode call. 0.3.0 introduced the structured report API, and 0.3.1 stabilized the issue formatting. SafeJSONDecoder is the higher-level convenience wrapper for app code that wants the same report alongside the decoded value.

let result = try SafeDecodingDiagnostics.capture {
    try JSONDecoder().decode(User.self, from: data)
}

let user = result.value
let report = result.report

if report.hasIssues {
    for issue in report.issues {
        print(issue.fieldPath, issue.errorDescription)
    }
}

This keeps the decoded model usable while giving the caller explicit access to the recovered-field issues.

Typed Fallbacks

Use SafeDecodingFallbackProvider when the field is required by your model shape but upstream data is noisy.

import SafeDecoding

enum UnknownCountryFallback: SafeDecodingFallbackProvider {
    static let fallbackValue = "ZZ"
}

struct Shipment: Decodable {
    let id: String
    @SafeFallbackDecodable<UnknownCountryFallback> var destinationCountryCode: String
}

Dirty vendor payload:

{
  "id": "shp_4815",
  "destinationCountryCode": 404
}

Decoded result:

let shipment = try JSONDecoder().decode(Shipment.self, from: data)
shipment.destinationCountryCode // "ZZ"

The fallback provider is explicit and typed, so the call site makes the recovery behavior visible in the model declaration instead of burying it in custom decoding code.

When To Use

Use SafeDecoding when:

  • you consume third-party or drift-prone APIs
  • one broken field should not discard the whole payload
  • you want typed defaults for required fields without writing custom init(from:)
  • you want to stay in the Codable model

Good Fits

  • third-party APIs with inconsistent optional field quality
  • apps that want to preserve valid model data instead of failing the whole decode
  • codebases that prefer a small wrapper-based entry point over manual parsing
  • models that need explicit fallback values such as "unknown", "ZZ", or sentinel enums
  • teams that want placeholder diagnostics today and richer reporting later

Weaker Fits

  • strict backend contracts you fully control
  • schema validation workflows
  • rich reporting pipelines that need more than placeholder diagnostics
  • cases where silent fallback values would hide contract breakage you should fail fast on

Runtime Semantics

  • @SafeDecodable is scoped to optional-like wrapped values
  • @LossySafeDecodable is scoped to array fields and skips malformed elements
  • @SafeFallbackDecodable uses the decoded value when decoding succeeds
  • if a fallback-backed field is present but malformed, a placeholder diagnostic is emitted and the provider value is used
  • the 0.3.0 reporting API is additive and non-breaking relative to 0.2.0, and 0.3.1 keeps that surface stable
  • missing safe fields decode to nil
  • broken safe fields emit a placeholder diagnostic and fall back to nil
  • nested wrapped fields recover and report full dot paths such as profile.name
  • lossy array issues report indexed paths such as users.1.id
  • missing lossy array fields recover to []
  • non-array values on lossy array fields recover to [] with one field-level issue
  • nested strict fields still fail normal decoding
  • diagnostics are intentionally lightweight; structured reports expose recoverable issues without changing strict Decodable failure boundaries

Stabilization Notes

0.7.0 is the pre-1.0 stabilization release. The package surface is now intended to be reviewed for long-term coherence rather than expanded with another major recovery primitive.

Documentation

README.md is the primary package documentation for the currently shipped package surface.

Swift Package Index metadata is configured in .spi.yml so the package page can reflect the current target and author metadata cleanly.

Testing

0.1.0 ships with Swift Testing coverage for valid values, missing keys, broken values, and diagnostic capture. 0.2.0 extends that coverage to typed fallback-backed decoding behavior, 0.3.0 adds report capture coverage that 0.3.1 keeps stable, 0.4.0 covers the SafeJSONDecoder entry point, 0.5.0 adds nested wrapper and nested report-path regression coverage, and 0.6.0 adds lossy array recovery and indexed issue-path coverage.

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun May 17 2026 18:18:13 GMT-0900 (Hawaii-Aleutian Daylight Time)