Skip to content

SmartCodable is a data parsing library built on Swift’s Codable, designed for simple usage and strong real-world compatibility. It gracefully handles missing fields, default values, and evolving JSON structures. SmartCodable 是基于 Swift Codable 的数据解析库,主打简单易用与真实业务场景下的强兼容性,能够优雅应对不断变化的 JSON 数据。

License

Notifications You must be signed in to change notification settings

iAmMccc/SmartCodable

Repository files navigation

SmartCodable

SmartCodable - Resilient & Flexible Codable for Swift

中文文档 Latest Release Swift 5.0+ Documentation SPM Supported MIT License Ask DeepWiki

SmartCodable redefines Swift data parsing by enhancing Apple's native Codable with production-ready resilience and flexibility. It provides seamless support for default values, nested flattening, and ignored properties, reducing boilerplate while increasing reliability.

Features

Compatibility

  • Robust Parsing – Handles missing keys, type mismatches, and null values safely.
  • Safe Defaults – Falls back to property initializers when parsing fails.
  • Smart Type Conversion – Converts common types automatically (e.g., Int ⇄ String, Bool ⇄ String).

Enhancements

  • Any & Collection Support – Parses Any, [Any], [String: Any] safely.
  • Nested Path Parsing – Decode nested JSON using designated paths.
  • Custom Value Transformation – Apply transformers for advanced conversions.
  • SmartFlat Support – Flatten nested objects into parent models seamlessly.
  • SmartPublished Support – Supports ObservableObject properties with real-time updates.
  • Inheritance Support – Enables model inheritance via @SmartSubclass.
  • Stringified JSON Parsing – Converts string-encoded JSON into objects or arrays automatically.

Convenience

  • Property Ignoring – Skip specific properties with @IgnoredKey, including non-Codable fields.
  • Flexible Input Formats – Deserialize from dictionaries, arrays, JSON strings, or Data.

Callbacks

  • Post-Processing CallbackdidFinishMapping() runs after decoding for custom initialization or adjustments.

Debugging

  • SmartSentinel Logging – Real-time parsing logs to track errors and data issues.

Quick Start

import SmartCodable

struct User: SmartCodableX {
    var name: String = ""
    var age: Int = 0
}

let user = User.deserialize(from: ["name": "John", "age": 30])

Explore & Contribute

Project / Tool Description
🔧 HandyJSON Step-by-step guide to replace HandyJSON with SmartCodable in your project.
🛠 SmartModeler Companion tool for converting JSON into SmartCodable Swift models.
👀 SmartSentinel Real-time parsing logs to track errors and issues. Supports.
💖 Contributing Support the development of SmartCodable through donations.
🏆 Contributors Key contributors to the SmartCodable codebase.

Installation

🛠 CocoaPods Installation

Version Installation Method Platform Requirements
Basic pod 'SmartCodable' iOS 13+ tvOS 15+ macOS 10.15+ watchOS 6.0+ visionOS 1.0+
Inheritance pod 'SmartCodable/Inherit' iOS 13+ macOS 11+

⚠️ Important Notes:

  • If you don't have strong inheritance requirements, the basic version is recommended

  • Inheritance features require Swift Macro support, Xcode 15+, and Swift 5.9+

📌 About Swift Macros Support (CocoaPods):

  • requires downloading swift-syntax dependencies for the first time (may take longer)
  • CocoaPods internally sets user_target_xcconfig["OTHER_SWIFT_FLAGS"] to load the macro plugin during build.
  • This may affect your main target's build flags and lead to subtle differences in complex projects or CI environments.
  • If needed, please open an issue for custom setups.

📦 Swift Package Manager

dependencies: [
    .package(url: "https://github.com/iAmMccc/SmartCodable.git", from: "xxx")
]

Documentation

1. The Basics

To conform to 'SmartCodable', a class need to implement an empty initializer

class BasicTypes: SmartCodableX {
    var int: Int = 2
    var doubleOptional: Double?
    required init() {}
}
let model = BasicTypes.deserialize(from: json)

For struct, since the compiler provide a default empty initializer, we use it for free.

struct BasicTypes: SmartCodableX {
    var int: Int = 2
    var doubleOptional: Double?
}
let model = BasicTypes.deserialize(from: json)

2. Deserialization API

2.1 deserialize

Only types conforming to SmartCodable (or [SmartCodable] for arrays) can use these methods

public static func deserialize(from dict: [String: Any]?, designatedPath: String? = nil,  options: Set<SmartDecodingOption>? = nil) -> Self?

public static func deserialize(from json: String?, designatedPath: String? = nil, options: Set<SmartDecodingOption>? = nil) -> Self?

public static func deserialize(from data: Data?, designatedPath: String? = nil, options: Set<SmartDecodingOption>? = nil) -> Self?

public static func deserializePlist(from data: Data?, designatedPath: String? = nil, options: Set<SmartDecodingOption>? = nil) -> Self?

1. Multi-Format Input Support

Input Type Example Usage Internal Conversion
Dictionary Model.deserialize(from: dict) Directly processes native collections
Array [Model].deserialize(from: arr) Directly processes native collections
JSON String Model.deserialize(from: jsonString) Converts to Data via UTF-8
Data Model.deserialize(from: data) Processes directly

2. Deep Path Navigation (designatedPath)

// JSON Structure:
{
  "data": {
    "user": {
      "info": { ...target content... }
    }
  }
}

// Access nested data:
Model.deserialize(from: json, designatedPath: "data.user.info")

Path Resolution Rules:

  1. Dot-separated path components
  2. Handles both dictionaries and arrays
  3. Returns nil if any path segment is invalid
  4. Empty path returns entire content

3. Decoding Strategies (options)

let options: Set<SmartDecodingOption> = [
    .key(.convertFromSnakeCase),
    .date(.iso8601),
    .data(.base64)
]
Strategy Type Available Options Description
Key Decoding .fromSnakeCase snake_case → camelCase
.firstLetterLower "FirstName" → "firstName"
.firstLetterUpper "firstName" → "FirstName"
Date Decoding .iso8601, .secondsSince1970, etc. Full Codable date strategies
Data Decoding .base64 Binary data processing
Float Decoding .convertToString, .throw NaN/∞ handling

⚠️ Important: Only one strategy per type is allowed (last one wins if duplicates exist)

2.2 Post-processing callback invoked after successful decoding

struct Model: SmartCodableX {
    var name: String = ""
    
    mutating func didFinishMapping() {
        name = "I am \(name)"
    }
}

3.2 Key Transformation

Defines key mapping transformations during decoding,First non-null mapping is preferred。

static func mappingForKey() -> [SmartKeyTransformer]? {
    return [
        CodingKeys.id <--- ["user_id", "userId", "id"],
        CodingKeys.joinDate <--- "joined_at"
    ]
}

4.3 Value Transformation

Convert between JSON values and custom types

Built-in Value Transformers

Transformer JSON Type Object Type Description
SmartDataTransformer String Data Converts between Base64 strings and Data objects
SmartHexColorTransformer String ColorObject Converts hex color strings to platform-specific color objects (UIColor/NSColor)
SmartDateTransformer Double/String Date Handles multiple date formats (timestamp Double or String) to Date objects
SmartDateFormatTransformer String Date Uses DateFormatter for custom date string formats
SmartURLTransformer String URL Converts strings to URLs with optional encoding and prefixing
struct Model: SmartCodableX {
    
    ...
    
    static func mappingForValue() -> [SmartValueTransformer]? {
        let format = DateFormatter()
        format.dateFormat = "yyyy-MM-dd"
        return [
            CodingKeys.url <--- SmartURLTransformer(prefix: "https://"),
            CodingKeys.date2 <--- SmartDateTransformer(),
            CodingKeys.date1 <--- SmartDateFormatTransformer(format)
        ]
    }
}

If you need additional parsing rules, Transformer will implement them yourself. Follow ValueTransformable to implement the requirements of the protocol.

public protocol ValueTransformable {
    associatedtype Object
    associatedtype JSON
    
    /// transform from ’json‘ to ’object‘
    func transformFromJSON(_ value: Any?) -> Object?
    
    /// transform to ‘json’ from ‘object’
    func transformToJSON(_ value: Object?) -> JSON?
}

Built-in Fast Transformer Helper

static func mappingForValue() -> [SmartValueTransformer]? {
    [
        CodingKeys.name <--- FastTransformer<String, String>(fromJSON: { json in
            "abc"
        }, toJSON: { object in
            "123"
        }),
        CodingKeys.subModel <--- FastTransformer<TestEnum, String>(fromJSON: { json in
            TestEnum.man
        }, toJSON: { object in
            object?.rawValue
        }),
    ]
}

3. propertyWrapper

3.1 @SmartAny

Codable does not support Any resolution, but can be implemented using @SmartAny。

struct Model: SmartCodableX {
    @SmartAny var dict: [String: Any] = [:]
    @SmartAny var arr: [Any] = []
    @SmartAny var any: Any?
}
let dict: [String: Any] = [
    "dict": ["name": "Lisa"],
    "arr": [1,2,3],
    "any": "Mccc"
]

let model = Model.deserialize(from: dict)
print(model)
// Model(dict: ["name": "Lisa"], arr: [1, 2, 3], any: "Mccc")

3.2 @IgnoredKey

If you need to ignore the parsing of attributes, you can override CodingKeys or use @IgnoredKey.

struct Model: SmartCodableX {
    @IgnoredKey
    var name: String = ""
}

let dict: [String: Any] = [
    "name": "Mccc"
]

let model = Model.deserialize(from: dict)
print(model)
// Model(name: "")

3.3 @SmartFlat

struct Model: SmartCodableX {
    var name: String = ""
    var age: Int = 0
  
    @SmartFlat
    var model: FlatModel?
   
}
struct FlatModel: SmartCodableX {
    var name: String = ""
    var age: Int = 0
}

let dict: [String: Any] =  [
    "name": "Mccc",
    "age": 18,
]

let model = Model.deserialize(from: dict)
print(model)
// Model(name: "Mccc", age: 18, model: FlatModel(name: "Mccc", age: 18))

3.4 @SmartPublished

class PublishedModel: ObservableObject, SmartCodable {
    required init() {}
    
    @SmartPublished
    var name: ABC?
}

struct ABC: SmartCodableX {
    var a: String = ""
}

if let model = PublishedModel.deserialize(from: dict) {
    model.$name
        .sink { newName in
            print("name updated,newValue is: \(newName)")
        }
        .store(in: &cancellables)
}

3.5 @SmartHexColor

Adds Codable support for UIColor/NSColor using hex string encoding/decoding.

struct Model: SmartCodableX {
    @SmartHexColor
    var color: UIColor?
}

let dict: [String: Any] = [
    "color": "7DA5E3"
]

let model = Model.deserialize(from: dict)
print(model)
// print: Model(color: UIExtendedSRGBColorSpace 0.490196 0.647059 0.890196 1)

4. Inheritance Support

This feature relies on Swift Macros, which requires Swift 5.9+ and is compatible with iOS 13+. Therefore, it is only supported in SmartCodable version 5.0 and above.

For using inheritance on lower versions, refer to: Inheritance in Lower Versions

If you need inheritance support, annotate your subclass with @SmartSubclass.

4.1 Basic Usage

class BaseModel: SmartCodableX {
    var name: String = ""
    required init() { }
}

@SmartSubclass
class StudentModel: BaseModel {
    var age: Int?
}

4.2 Subclass Implements Protocol Method

Just implement it directly—no need for the override keyword.

class BaseModel: SmartCodableX {
    var name: String = ""
    required init() { }
    
    class func mappingForKey() -> [SmartKeyTransformer]? {
        retrun nil
    }
}

@SmartSubclass
class StudentModel: BaseModel {
    var age: Int?
    
    override static func mappingForKey() -> [SmartKeyTransformer]? {
        [ CodingKeys.age <--- "stu_age" ]
    }
}

4.3 Parent Class Implements Protocol Method

class BaseModel: SmartCodableX {
    var name: String = ""
    required init() { }
    
    static func mappingForKey() -> [SmartKeyTransformer]? {
        [ CodingKeys.name <--- "stu_name" ]
    }
}

@SmartSubclass
class StudentModel: BaseModel {
    var age: Int?
}

4.4 Both Parent and Subclass Implement Protocol Method

A few things to note:

  • The protocol method in the parent class must be marked with class.
  • The subclass should call the parent class's implementation.
class BaseModel: SmartCodableX {
    var name: String = ""
    required init() { }
    
    class func mappingForKey() -> [SmartKeyTransformer]? {
        [ CodingKeys.name <--- "stu_name" ]
    }
}

@SmartSubclass
class StudentModel: BaseModel {
    var age: Int?
    
    override static func mappingForKey() -> [SmartKeyTransformer]? {
        let trans = [ CodingKeys.age <--- "stu_age" ]
        
        if let superTrans = super.mappingForKey() {
            return trans + superTrans
        } else {
            return trans
        }
    }
}

5. Special support

5.1 Smart Stringified JSON Parsing

SmartCodable automatically handles string-encoded JSON values during decoding, seamlessly converting them into nested model objects or arrays while maintaining all key mapping rules.

  • Automatic Parsing: Detects and decodes stringified JSON ("{\"key\":value}") into proper objects/arrays
  • Recursive Mapping: Applies mappingForKey() rules to parsed nested structures
  • Type Inference: Determines parsing strategy (object/array) based on property type
struct Model: SmartCodableX {
    var hobby: Hobby?
    var hobbys: [Hobby]?
}

struct Hobby: SmartCodableX {
    var name: String = ""
}

let dict: [String: Any] = [
    "hobby": "{\"name\":\"sleep1\"}",
    "hobbys": "[{\"name\":\"sleep2\"}]",
]

guard let model = Model.deserialize(from: dict) else { return }

5.2 Compatibility

If attribute resolution fails, SmartCodable performs compatibility processing for thrown exceptions. Ensure that the entire parsing is not interrupted. Even better, you don't have to do anything about it.

let dict = [
    "number1": "123",
    "number2": "Mccc",
    "number3": "Mccc"
]

struct Model: SmartCodableX {
    var number1: Int?
    var number2: Int?
    var number3: Int = 1
}

// decode result
// Model(number1: 123, number2: nil, number3: 1)

Type conversion compatibility

When the data is parsed, the type cannot be matched. Raises a.typeMismatch error. SmartCodable will attempt to convert data of type String to the desired type Int.

Default Fill compatible

When the type conversion fails, the initialization value of the currently parsed property is retrieved for padding.

5.3 parse very large data

When you parse very large data, try to avoid the compatibility of parsing exceptions, such as: more than one attribute is declared in the attribute, and the declared attribute type does not match.

Do not use @IgnoredKey when there are attributes that do not need to be parsed, override CodingKeys to ignore unwanted attribute parsing.

This can greatly improve the analytical efficiency.

5.4 The Enum

To be convertable, An enum must conform to SmartCaseDefaultable protocol. Nothing special need to do now.

struct Student: SmartCodableX {
    var name: String = ""
    var sex: Sex = .man

    enum Sex: String, SmartCaseDefaultable {
        case man = "man"
        case woman = "woman"
    }
}
let model = Student.deserialize(from: json)

Decoding of associative value enum

Make the enumeration follow SmartAssociatedEnumerable。Override the mappingForValue method and take over the decoding process yourself.

struct Model: SmartCodableX {
    var sex: Sex = .man
    static func mappingForValue() -> [SmartValueTransformer]? {
        [
            CodingKeys.sex <--- RelationEnumTranformer()
        ]
    }
}

enum Sex: SmartAssociatedEnumerable {    
    case man
    case women
    case other(String)
}

struct RelationEnumTranformer: ValueTransformable {
    typealias Object = Sex
    typealias JSON = String

    func transformToJSON(_ value: Introduce_8ViewController.Sex?) -> String? {
        // do something
    }
    func transformFromJSON(_ value: Any?) -> Sex? {
        // do something
    }
}

5.5 Update Existing Model

It can accommodate any data structure, including nested array structures.

struct Model: SmartCodableX {
    var name: String = ""
    var age: Int = 0
}

var dic1: [String : Any] = [
    "name": "mccc",
    "age": 10
]
let dic2: [String : Any] = [
    "age": 200
]
guard var model = Model.deserialize(from: dic1) else { return }
SmartUpdater.update(&model, from: dic2)

// now: model is ["name": mccc, "age": 200].

FAQ

If you're looking forward to learning more about the Codable protocol and the design thinking behind SmartCodable, check it out.

👉 learn SmartCodable

👉 github discussions

👉 SmartCodable Test

Github Stars

Stars

Join Community 🚀

SmartCodable is an open-source project dedicated to making Swift data parsing more robust, flexible and efficient. We welcome all developers to join our community!

JoinUs

About

SmartCodable is a data parsing library built on Swift’s Codable, designed for simple usage and strong real-world compatibility. It gracefully handles missing fields, default values, and evolving JSON structures. SmartCodable 是基于 Swift Codable 的数据解析库,主打简单易用与真实业务场景下的强兼容性,能够优雅应对不断变化的 JSON 数据。

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 7