一個強大的 Swift Macros 套件,為 JSON 解碼提供自動預設值支援,徹底解決了 Property Wrapper + Codable 的技術限制。 使用 Swift 5.9 引入的 Macro 來解決 Codable 中一個常見的痛點:後端回傳的資料型別不穩定或是有缺漏時,容易導致解碼失敗。
A powerful Swift Macros suite that provides automatic default value support for JSON decoding, completely solving the technical limitations of Property Wrapper + Codable. It leverages Swift 5.9's Macro feature to solve a common pain point in Codable: when backend data types are unstable or missing, it often leads to decoding failures.
在真實世界的開發場景中,API 回傳的 JSON 品質參差不齊,AutoCodable 透過提供預設值和型別轉換,大幅提高了 JSON 解碼的穩定性與容錯能力,避免了因為單一欄位解碼失敗而導致整個物件解碼失敗的問題。且語法簡潔且直觀:透過 @AutoCodable 和 @DecodeDefault 這兩個 attached Macro,開發者可以很輕易地為特定屬性加上解碼保護,而不需要手動撰寫大量的 init(from:) 和 CodingKeys,讓 Model 的宣告變得非常乾淨。透過 Swift Macro,AutoCodable 在編譯時期自動生成 Codable,行時期的效能與手寫 Codable 幾乎沒有差異,且可以在編譯時期就能發現錯誤,而不是在執行時期才 Crash。可以透過 Xcode 的 "Expand Macro" 功能,清楚地看到 Macro 生成的程式碼,方便除錯。
In real-world development scenarios, the quality of JSON returned by APIs is inconsistent. AutoCodable greatly improves the stability and fault tolerance of JSON decoding by providing default values and type conversion, avoiding the problem of the entire object failing to decode due to a single field decoding failure. The syntax is concise and intuitive: through the @AutoCodable and @DecodeDefault attached macros, developers can easily add decoding protection to specific properties without having to manually write a large amount of init(from:) and CodingKeys, making the Model declaration very clean. Through Swift Macro, AutoCodable automatically generates Codable at compile time, the runtime performance is almost the same as handwritten Codable, and errors can be found at compile time instead of crashing at runtime. You can clearly see the code generated by the Macro through Xcode's "Expand Macro" function, which is convenient for debugging.
@AutoCodable
struct Model: Codable {
@DecodeDefault("") var name: String // Use "" when decoding fails
@DecodeDefault("Tom") var nickname: String // Use "Tom" when decoding fails
@DecodeDefault(100) var score: Int // Use 100 when decoding fails
@DecodeDefault(.daily) var type: TypeEnum // Use .daily when decoding fails
}- ❌ 無全域變數 / No Global Variables:沒有線程安全問題 / No thread safety issues
- ❌ 無記憶體洩漏 / No Memory Leaks:編譯時生成,無靜態存儲 / Compile-time generation, no static storage
- ❌ 無運行時開銷 / No Runtime Overhead:完全在編譯時處理 / Completely handled at compile time
- ✅ 完全類型安全 / Completely Type Safe:編譯時檢查所有類型 / All types checked at compile time
- ✅ 支援所有 Codable 類型 / Support All Codable Types:包括自定義 enum / Including custom enums
@AutoCodable
struct User: Codable {
@DecodeDefault("Unknown") var name: String
@DecodeDefault(0) var age: Int
@DecodeDefault(true) var isActive: Bool
// Required properties (must exist in JSON)
var email: String
var userId: String
}let json = """
{
"email": "test@example.com",
"userId": "12345"
// Note: Missing name, age, isActive intentionally
}
"""
let user = try JSONDecoder().decode(User.self, from: json.data(using: .utf8)!)
print(user.name) // "Unknown" (using default value)
print(user.age) // 0 (using default value)
print(user.isActive) // true (using default value)
print(user.email) // "test@example.com" (from JSON)/* Your code */
@AutoCodable
struct Model: Codable {
@DecodeDefault("Tom") var name: String
@DecodeDefault(100) var score: Int
}
/* Expanded Source */
struct Model: Codable {
var name: String
var score: Int
// Following auto-generated code by Macro
enum CodingKeys: String, CodingKey {
case name = "name"
case score = "score"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? "Tom"
self.score = (try? container.decode(Int.self, forKey: .score)) ?? 100
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(score, forKey: .score)
}
}@AutoCodable
struct BasicTypes: Codable {
@DecodeDefault() var optional: String?
@DecodeDefault("") var text: String
@DecodeDefault(0) var number: Int
@DecodeDefault(0.0) var decimal: Double
@DecodeDefault(false) var flag: Bool
}@AutoCodable
struct Collections: Codable {
@DecodeDefault([]) var items: [String]
@DecodeDefault([:]) var metadata: [String: String]
@DecodeDefault(Set<String>()) var tags: Set<String>
}enum Status: String, Codable {
case active, inactive, pending
}
@AutoCodable
struct WithEnum: Codable {
@DecodeDefault(Status.pending) var status: Status
}@AutoCodable
@GenerateEmptyInit
struct Address: Codable {
@DecodeDefault("") var street: String
@DecodeDefault("") var city: String
}
extension Address {
static let `default` = Address()
}
@AutoCodable
struct User: Codable {
@DecodeDefault("") var name: String
@DecodeDefault(Address.default) var address: Address
}@AutoCodable
@GenerateEmptyInit
struct Settings: Codable {
@DecodeDefault(true) var notifications: Bool
@DecodeDefault("light") var theme: String
@DecodeDefault(60) var timeout: Int
@DecodeIgnore var sessionId: String? = nil
}
// Create instance with default values
let defaultSettings = Settings()
print(defaultSettings.notifications) // true
print(defaultSettings.theme) // "light"
print(defaultSettings.timeout) // 60@AutoCodable
@GenerateMemberwiseInit
struct Product: Codable {
@DecodeDefault("") var name: String
@DecodeDefault(0.0) var price: Double
@DecodeDefault(true) var isAvailable: Bool
@DecodeIgnore var createdAt: Date = Date()
}
// Create instance with custom values
let product = Product(
name: "MacBook Pro",
price: 2999.99,
isAvailable: false
)
print(product.name) // "MacBook Pro"
print(product.price) // 2999.99
print(product.isAvailable) // false@AutoCodable
@GenerateEmptyInit // Company()
@GenerateMemberwiseInit // Company(name:industry:location:)
struct Company: Codable {
@DecodeDefault("") var name: String
@DecodeDefault("") var industry: String
@DecodeDefault("TW") var location: String
}
// Both initialization methods available
let defaultCompany = Company() // Use default
let customCompany = Company( // Use custom values
name: "Apple",
industry: "Technology",
location: "US"
)
// Usage in extensions
extension Company {
static let `default` = Company() // Use default init
static let tech = Company( // Use All parameters init
name: "Tech Corp",
industry: "Technology",
location: "TW"
)
}@AutoCodable
@GenerateEmptyInit
@GenerateMemberwiseInit
struct DatabaseConfig: Codable {
@DecodeDefault("localhost") var host: String
@DecodeDefault(5432) var port: Int
@DecodeDefault("myapp") var database: String
}
extension DatabaseConfig {
static let `default` = DatabaseConfig()
}
@AutoCodable
@GenerateEmptyInit
@GenerateMemberwiseInit
struct AppConfig: Codable {
@DecodeDefault("MyApp") var appName: String
@DecodeDefault("1.0.0") var version: String
@DecodeDefault(DatabaseConfig.default) var database: DatabaseConfig
@DecodeIgnore var buildTimestamp: Date = Date()
}
// Flexible creation methods
let config1 = AppConfig() // Default
let config2 = AppConfig( // Custom top-level properties
appName: "MyCustomApp",
version: "2.0.0",
database: DatabaseConfig()
)
let config3 = AppConfig( // Fully custom
appName: "ProductionApp",
version: "1.5.0",
database: DatabaseConfig(
host: "prod-db.company.com",
port: 5432,
database: "production"
)
)使用 @GenerateEmptyInit 或 @GenerateMemberwiseInit 時,必須遵循以下規則:
When using @GenerateEmptyInit or @GenerateMemberwiseInit, you must follow these rules:
@AutoCodable
@GenerateEmptyInit
@GenerateMemberwiseInit
struct ValidModel: Codable {
@DecodeDefault("") var name: String // ✅ Marked @DecodeDefault
@DecodeDefault(0) var age: Int // ✅ Marked @DecodeDefault
@DecodeIgnore var cache: String? = nil // ✅ Marked @DecodeIgnore
}
@AutoCodable
@GenerateMemberwiseInit
struct InvalidModel: Codable {
@DecodeDefault("") var name: String
var unmarkedProperty: String // ❌ Unmarked property: unmarkedProperty
}@AutoCodable
struct TypeConversion: Codable {
// String to Int conversion
@DecodeDefault(0, from: String.self) var stringToInt: Int
// String to Bool conversion
@DecodeDefault(false, from: String.self) var stringToBool: Bool
// Handle multiple source types
@DecodeDefault(0, from: [Int.self, String.self]) var flexibleInt: Int
}@AutoCodable
struct CustomKeys: Codable {
@DecodeDefault("", keyName: "user_name") var userName: String
@DecodeDefault(0, keyName: "user_id") var userId: Int
@DecodeDefault(false, keyName: "is_verified") var isVerified: Bool
}@AutoCodable
struct WithIgnoredProperties: Codable {
@DecodeDefault("") var name: String
@DecodeDefault(0) var age: Int
@DecodeIgnore var computedValue: String = "calculated"
@DecodeIgnore var timestamp: Date = Date()
}@AutoCodable
struct OptionalDefaults: Codable {
@DecodeDefault() var optionalName: String? // Defaults to nil
@DecodeDefault() var optionalAge: Int? // Defaults to nil
@DecodeDefault("Guest") var nameWithDefault: String // Defaults to "Guest"
}@AutoCodable
struct Product: Codable {
// Properties with default values
@DecodeDefault("") var name: String
@DecodeDefault(0.0) var price: Double
@DecodeDefault(true) var isAvailable: Bool
@DecodeDefault([]) var tags: [String]
// Required properties (must exist in JSON)
var productId: String
var createdAt: Date
// Optional properties
var description: String?
var imageUrl: String?
}@AutoCodable
struct EdgeCases: Codable {
// Handle whitespace in string-to-number conversion
@DecodeDefault(0, from: String.self) var numberFromString: Int
// Handle NaN for floating point
@DecodeDefault(0.0, from: String.self) var doubleFromString: Double
// Handle mixed character strings (will use default)
@DecodeDefault(0, from: String.self) var intFromMixedString: Int
}
// JSON Test Data
let json = """
{
"numberFromString": " 123 ", // -> 123 (trimmed)
"doubleFromString": "NaN", // -> Double.nan
"intFromMixedString": "123有中文" // -> 0 (default, conversion fails)
}
"""- ✅ 基本 Property Wrapper 功能 / Basic Property Wrapper functionality
- ✅ Macro 展開正確性 / Macro expansion correctness
- ✅ 錯誤情況處理 / Error condition handling
- ✅ 複雜類型支援 / Complex type support
- ✅ 實際 JSON 解碼場景 / Real JSON decoding scenarios
- ✅ 類型轉換測試 / Type conversion testing
- ✅ 邊緣情況測試 / Edge case testing
- 右鍵點擊
@AutoCodable/ Right-click on@AutoCodable - 選擇 "Expand Macro" / Select "Expand Macro"
- 查看生成的完整代碼 / View the complete generated code
傳統的 Property Wrapper + Codable 組合有根本性的技術問題:
Traditional Property Wrapper + Codable combination has fundamental technical issues:
@propertyWrapper
struct DecodeDefault<T: Codable>: Codable {
init(_ defaultValue: T) { /* User-specified default value */ }
init(from decoder: Decoder) throws {
// ⚠️ Problem: Cannot access the default value passed by user during initialization!
}
}protocol DefaultValueProvider {
associatedtype Value: Codable
static var defaultValue: Value { get }
}
@propertyWrapper
struct Default<Provider: DefaultValueProvider>: Codable {
var wrappedValue: Provider.Value = Provider.defaultValue
// ... decoder implement ...
}
//
struct One: DefaultValueProvider { static var defaultValue = 1 }
struct Zero: DefaultValueProvider { static var defaultValue = 0 }
struct True: DefaultValueProvider { static var defaultValue = true }
// ⚠️ If you need a different default value (e.g., 100), you would have to define a new struct for each value.
struct OneHundred: DefaultValueProvider { static var defaultValue = 100 }Swift Macros 在編譯時運行,能夠:
Swift Macros run at compile time and can:
- 📖 編譯時分析 / Compile-time Analysis:Macro 讀取你的源碼並分析
@DecodeDefault屬性 / Macro reads your source code and analyzes@DecodeDefaultattributes - 🏗️ 代碼生成 / Code Generation:自動生成完整的
Codable實現 / Automatically generates completeCodableimplementation - 🔒 類型安全 / Type Safety:所有類型檢查在編譯時完成 / All type checking completed at compile time
- 初次編譯可能增加 2-5 秒 / Initial compilation may increase by 2-5 seconds
- 增量編譯影響極小 / Minimal impact on incremental compilation
- 生成的代碼量與手寫代碼相當 / Generated code amount equivalent to hand-written code
- 影響 < 0.1%,可忽略 / Impact < 0.1%, negligible
- 零運行時開銷 / Zero runtime overhead
- 與手寫 Codable 代碼性能完全相同 / Exactly the same performance as hand-written Codable code
- 🎯 完美語法 / Perfect Syntax:語法簡潔自然,直接在屬性上聲明預設值,可讀性極高。/ Clean and natural syntax, allowing you to declare default values directly on properties for high readability.
- 🚀 減少代碼 / Reduce Code:自動生成複雜的 Codable 實現 / Automatically generate complex Codable implementation
- 🔍 易於調試 / Easy to Debug:可以查看生成的代碼 / Can view generated code
- 📝 類型提示 / Type Hints:完整的 IDE 支援 / Complete IDE support
- ⚡ 零運行時開銷 / Zero Runtime Overhead:編譯時處理 / Compile-time processing
- 🔒 完全類型安全 / Completely Type Safe:編譯時檢查 / Compile-time checking
- 🧵 無線程問題 / No Threading Issues:無全域狀態 / No global state
- 💾 無記憶體洩漏 / No Memory Leaks:無靜態存儲 / No static storage
- 📚 豐富文檔 / Rich Documentation:完整的使用指南 / Complete usage guide
- 🧪 完整測試 / Complete Testing:涵蓋各種使用場景 / Cover various usage scenarios
- 🔄 易於擴展 / Easy to Extend:可添加新功能 / Can add new features
- 🛠️ 工具支援 / Tool Support:測試和調試工具 / Testing and debugging tools
- 核心 DecodeDefault Property Wrapper / Core DecodeDefault Property Wrapper
- AutoCodable Macro 實現 / AutoCodable Macro implementation
- @GenerateEmptyInit 宏實現 / @GenerateEmptyInit Macro implementation
- @GenerateMemberwiseInit 宏實現 / @GenerateMemberwiseInit Macro implementation
- 自動空初始化器生成 / Automatic empty initializer generation
- 自動成員初始化器生成 / Automatic memberwise initializer generation
- 自訂類型預設值支援 / Custom type default value support
- 完整測試套件 / Complete test suite
- 使用文檔和範例 / Usage documentation and examples
- 類型轉換支援 / Type conversion support
- 邊緣情況處理 / Edge case handling
- 結構性測試驗證 / Structural test validation
- 支援更多預設值類型(Date, URL 等)/ Support more default value types (Date, URL, etc.)
- 添加自定義錯誤處理 / Add custom error handling
- 支援嵌套 Macro 展開 / Support nested Macro expansion
- 性能優化和更好的錯誤訊息 / Performance optimization and better error messages
- SwiftUI 整合範例 / SwiftUI integration examples
如果遇到任何問題,請檢查 / If you encounter any issues, please check:
- Swift 版本是否 >= 5.9 / Swift version >= 5.9
- 是否正確添加了 Package 依賴 / Package dependency correctly added
- 是否在正確的類型上使用了
@AutoCodable/@AutoCodableused on correct types
📋 查看完整變更記錄 / Full Changelog: CHANGELOG.md
AutoCodable Package - Make JSON decoding simpler, safer, and more elegant! 🎉