SmartCodable Documentation

repository·main·Indexed 20 days ago

https://github.com/iammccc/smartcodable

A Swift library that enhances the native Codable protocol with production-ready resilience. SmartCodable prevents entire model parsing failures by gracefully handling missing keys, type mismatches, and null values using property defaults and automatic type conversion. It includes specialized property wrappers like @SmartAny, @SmartFlat, and @SmartDate, as well as the @SmartSubclass macro for simplified class inheritance support in Swift 5.9+.

Tokens
13.5K
Snippets
45
Records
60
Agent score
72%

What's inside SmartCodable

  1. Implementation details for Property Wrappers and KeyedContainers

    main

    Developers implementing or modifying core logic should be aware of these internal behaviors:

    • Property Wrapper Storage: Swift stores property wrappers with an underscore prefix (e.g., @SmartAny var name is stored as _name). The DecodingCache handles this mapping.
    • KeyedContainer Initialization: The _convertDictionary() method in KeyedContainer is executed exactly once during container initialization. All subsequent property decoding relies on this transformed dictionary.
    • Patcher Type Conversion: Type conversions within a Patcher (e.g., StringInt) must be bi-directionally safe. You must validate that the input is valid (e.g., a valid numeric string) rather than silently returning a default like 0.
  2. Performance and Debugging considerations

    main

    SmartCodable is optimized for production environments using the following patterns:

    • Lazy Reflection: Mirror reflection is only performed by DecodingCache when default values are explicitly required for the first time, not on every decoding cycle.
    • Thread Safety: SafeDictionary uses NSLock to protect the Sentinel logging dictionary.
    • Zero-Overhead Logging: The SmartSentinel logging system uses a guard clause (guard debugMode != .none else { return }) at every entry point. In Release environments where debugMode is set to .none, logging incurs zero overhead.
  3. How SmartCodable handles decoding errors

    main

    Unlike native Swift Codable, which throws an exception and fails the entire model parsing when encountering missing fields, type mismatches, or null values, SmartCodable implements a graceful degradation strategy.

    When a property fails to decode, the library follows this priority order to prevent failure:

    1. Type Conversion (Patcher): Attempts to automatically convert types (e.g., converting a JSON string "123" to an Int).
    2. Default Value Fallback (DecodingCache): If conversion fails, it falls back to the initial value declared in your model (e.g., var name: String = "Default").
    3. Logging (SmartSentinel): If all else fails, it logs the issue without interrupting the parsing process.
  4. How SmartCodable handles decoding failures

    main

    Standard Codable throws a DecodingError if a field is missing, is null, or has an incorrect type. SmartCodable provides fault tolerance by attempting to resolve these issues using a specific fallback hierarchy.

    When a decoding error occurs, SmartCodable follows this priority order to find a compatible value:

    1. Type Transformation: Attempts to convert the existing value to the target type (e.g., converting the string "true" to a Bool).
    2. Property Initial Value: If transformation fails, it uses the value assigned to the property during the model's initialization (e.g., var name: String = "Default").
    3. Type Default Value: If no initial value is available, it falls back to a type-level default value defined via the Defaultable protocol.
  5. How SmartCodable improves upon standard Codable

    main

    SmartCodable provides resilience against common JSON parsing failures that typically cause standard Apple Codable to throw errors and fail the entire model parsing process.

    ScenarioStandard CodableSmartCodable
    Missing key❌ Throws keyNotFound, entire model fails✅ Uses property initializer as default
    Type mismatch (e.g., "123" for Int)❌ Throws typeMismatch, entire model fails✅ Auto-converts, returns 123
    Null value for non-optional❌ Throws valueNotFound, entire model fails✅ Falls back to default value
    Extra unknown keys✅ Ignored✅ Ignored
  6. Combine Protocols and @SmartFlat for flexible inheritance

    main

    You can combine Protocol and @SmartFlat to get the best of both worlds: the type safety of protocols and the automated parsing of @SmartFlat. This avoids the repetition of the Protocol-only approach and the lack of common types in the @SmartFlat-only approach.

    Pros:

    • No manual encoding/decoding implementation.
    • Avoids property repetition in subclasses.
    • Provides a common type constraint via the protocol.

    Cons:

    • It is not true class inheritance.
    protocol ManBaseModelProtocol {
        var manBase: BaseModel { set get }
    }
    
    class BaseModel: SmartCodable {
        required init() {}
        
        var name: String = ""
        var sex: Int = 0
    }
    
    class SubModel: SmartCodable, ManBaseModelProtocol {
        required init() {}
        
        @SmartFlat
        var manBase: BaseModel = .init()
        
        var age: Int = 0
    }
  7. Thread-safe configuration with SmartCodableOptions

    main

    The SmartCodableOptions struct provides global configuration for decoding behavior. In version 6.1.0+, access to these global settings is thread-safe using internal locking. This ensures that modifying configuration from one thread while decoding on another does not cause data races.

    Key configuration properties include:

    • numberStrategy: Controls how numbers are converted during decoding.
    • ignoreNull: Determines whether null values should be ignored during decoding.
  8. Implement inheritance via Protocols

    main

    For lightweight sharing of properties, you can use a Protocol-based approach. Define a protocol for the common properties and have your subclasses conform to it.

    Pros:

    • No manual implementation of subclass encoding/decoding logic required.
    • Subclasses share a common type via the protocol.

    Cons:

    • Every subclass must manually declare and implement the properties defined in the protocol.
    protocol BaseModel {
        var name: String { set get }
        var sex: Int { set get }
    }
    
    class SubModel: BaseModel, SmartCodable {
        required init() {}
        
        var name: String = ""
        var sex: Int = 0
        
        var age: Int = 0
    }
  9. Use @SmartFlat for composition-based parsing

    main

    If you want to avoid the complexities of inheritance, you can use composition with the @SmartFlat property wrapper. This allows you to embed a base model as a property within a subclass. The @SmartFlat wrapper will extract data from the current JSON node and populate the embedded property, rather than looking for a nested object.

    Pros:

    • No need to manually implement subclass encoding/decoding logic.
    • Avoids the repetition required by the Protocol-based approach.

    Cons:

    • Subclasses do not share a common type (they are not related via inheritance or protocols).
    class BaseModel: SmartCodable {
        required init() {}
        
        var name: String = ""
        var sex: Int = 0
    }
    
    class SubModel: SmartCodable {
        required init() {}
        
        var age: Int = 0
        
        @SmartFlat
        var manBase: BaseModel = .init()
    }
    
    let dict = [
            "name": "小明",
            "sex": 1,
            "age": 10,
    ] as [String : Any]
    
    guard let model = SubModel.deserialize(from: dict) else { return }
    print(model.manBase.name) // 小明
    print(model.manBase.sex) // 1
    print(model.age) // 10
  10. Interpret Smart Sentinel parsing logs

    main

    Smart Sentinel provides a visual tree representation of the parsing process. It highlights specific issues such as:

    • Type mismatches: e.g., Expected to decode 'Int' but found 'String' instead.
    • Missing keys: e.g., No value associated with key.
    • Null values: e.g., Expected to decode 'Int' but found 'null' instead.

    This allows you to trace exactly where in a nested JSON structure the data deviates from your Swift models.

    ================================  [Smart Sentinel]  ================================
    Array<SomeModel> 👈🏻 👀
       ╆━ Index 0
          ┆┄ a: Expected to decode 'Int' but found 'String' instead.
          ┆┄ b: Expected to decode 'Int' but found 'Array' instead.
          ┆┄ c: No value associated with key.
    ...
  11. Manual inheritance implementation (Standard Codable style)

    main

    If you cannot use @SmartSubclass (e.g., on older Swift/Xcode versions), you can implement inheritance manually by overriding init(from:) and encode(to:). This is similar to how standard Swift Codable works but uses SmartCodable to benefit from enhanced error tolerance (handling missing fields, type mismatches, etc.).

    Pros:

    • Follows standard Codable patterns.
    • Supports type mismatch, missing fields, and nil scenarios.

    Cons:

    • Requires significant boilerplate in subclasses.
    class BaseModel: SmartCodable {
        var name: String = ""
        required init() { }
    }
    
    class SubModel: BaseModel {
        var age: Int = 0
        
        private enum CodingKeys: CodingKey {
            case age
        }
        
        required init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.age = try container.decode(Int.self, forKey: .age)
            try super.init(from: decoder)
        }
        
        override func encode(to encoder: Encoder) throws {
            try super.encode(to: encoder)
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(age, forKey: .age)
        }
        
        required init() { super.init() }
    }
  12. Handle inheritance with @SmartSubclass (Recommended)

    main

    For SmartCodable 5.0+, the recommended way to handle class inheritance is using the @SmartSubclass macro. This macro automatically generates CodingKeys, init(from:), and encode(to:) at compile time, allowing subclasses to inherit properties from a base class without manual boilerplate.

    Requirements:

    • Swift 5.9+
    • Xcode 15+

    Usage: Apply @SmartSubclass to the subclass that inherits from a SmartCodable base class.

    class BaseModel: SmartCodable {
        var name: String = ""
        required init() { }
    }
    
    @SmartSubclass
    class Model: BaseModel {
        var age: Int = 0
    }
    
    let dict = ["name": "小明", "age": 10] as [String : Any]
    guard let model = Model.deserialize(from: dict) else { return }
    print(model.age)   // 10
    print(model.name)  // 小明