Codable Macros Make Swift Serialization So Simple!

Codable Macros Make Swift Serialization So Simple! Hello everyone! As Swift developers, we deal with data every day, and the conversion between JSON and models is undoubtedly a daily task. Apple provided us with the Codable protocol, which performs well in many situations, but as business logic becomes more complex, we often find ourselves stuck writing a lot of boilerplate code: manually defining CodingKeys, implementing init(from:) and encode(to:), handling nested structures, dealing with different naming styles, parsing various date formats... These tedious tasks are not only time-consuming but also error-prone. Is there a more elegant and efficient way to handle Codable in Swift? The answer is definitely yes! With Swift Macros introduced in Swift 5.9+, the possibilities for code generation have been greatly expanded. Today, I'm introducing a framework built on Swift Macros called ReerCodable! ReerCodable (https://github.com/reers/ReerCodable) aims to completely simplify the Codable experience through declarative annotations, allowing you to say goodbye to tedious boilerplate code and focus on business logic itself. Practical Application Example Let's look at a practical example to see how ReerCodable simplifies development work. Suppose we have a complex API response: { "code": 0, "data": { "user_info": { "user_name": "phoenix", "birth_date": "1990-01-01T00:00:00Z", "location": { "city": "Beijing", "country": "China" }, "height_in_meters": 1.85, "is_vip": true, "tags": ["tech", null, "swift"], "last_login": 1731585275944 } } } Using ReerCodable, we can define our model like this: @Codable struct ApiResponse { var code: Int @CodingKey("data.user_info") var userInfo: UserInfo } @Codable @SnakeCase struct UserInfo { var userName: String @DateCoding(.iso8601) var birthDate: Date @CodingKey("location.city") var city: String @CodingKey("location.country") var country: String @CustomCoding( decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 }, encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") } ) var heightInCentimeters: Double var isVip: Bool @CompactDecoding var tags: [String] @DateCoding(.millisecondsSince1970) var lastLogin: Date } // Usage do { // The original way remains unchanged let resp = try JSONDecoder().decode(ApiResponse.self, from: jsonString.data(using: .utf8)!) // Convenient method provided by ReerCodable let response = try ApiResponse.decode(from: jsonString) print("Username: \(response.userInfo.userName)") print("Birth date: \(response.userInfo.birthDate)") print("Height(cm): \(response.userInfo.heightInCentimeters)") } catch { print("Parsing failed: \(error)") } The "Pain" of Native Codable Before we dive into the magic of ReerCodable, let's review the common pain points when using native Codable: Manual CodingKeys: When JSON keys don't match property names, you need to manually write the CodingKeys enum. Even if you only modify one property, you have to write all other properties as well. This might be manageable with few properties, but becomes a nightmare once you have many. Nested Keys: When dealing with deeply nested JSON data, you need to define multiple intermediate structures or manually write decoding logic. Naming Style Conversion: Backend returns snake_case or kebab-case, while Swift recommends camelCase, requiring manual mapping. Complex Decoding Logic: If you need custom decoding (type conversion, data fixing, etc.), you have to implement init(from:). Default Value Handling: For non-Optional properties missing in JSON, an exception will be thrown even if a default value exists. Even Optional enums can fail decoding. Ignoring Properties: Some properties don't need to participate in encoding/decoding, requiring manual handling in CodingKeys or implementations. Various Date Formats: Timestamps, ISO8601, custom formats... require configuring different dateDecodingStrategy for JSONDecoder or manual handling. null in Collections: When arrays or dictionaries contain null values, decoding fails if the corresponding type is non-Optional. Inheritance: Parent class properties can't be automatically handled in the child class's Codable implementation. Enum Handling: Enums with associated values or those needing to match multiple raw values have limited support in native Codable. Community Status To solve JSON serialization problems, the Swift community has produced many excellent third-party frameworks. Understanding their design philosophies and pros/cons helps us better understand why Swift Macros-based solutions are a better choice today. 1. Frameworks Based on Custom Protocols ObjectMapper ObjectMapper is one of the earliest Swift JSON parsing libraries, based on a

May 8, 2025 - 13:20
 0
Codable Macros Make Swift Serialization So Simple!

Codable Macros Make Swift Serialization So Simple!

Hello everyone! As Swift developers, we deal with data every day, and the conversion between JSON and models is undoubtedly a daily task. Apple provided us with the Codable protocol, which performs well in many situations, but as business logic becomes more complex, we often find ourselves stuck writing a lot of boilerplate code: manually defining CodingKeys, implementing init(from:) and encode(to:), handling nested structures, dealing with different naming styles, parsing various date formats... These tedious tasks are not only time-consuming but also error-prone.

Is there a more elegant and efficient way to handle Codable in Swift?

The answer is definitely yes! With Swift Macros introduced in Swift 5.9+, the possibilities for code generation have been greatly expanded. Today, I'm introducing a framework built on Swift Macros called ReerCodable!

ReerCodable (https://github.com/reers/ReerCodable) aims to completely simplify the Codable experience through declarative annotations, allowing you to say goodbye to tedious boilerplate code and focus on business logic itself.

Practical Application Example

Let's look at a practical example to see how ReerCodable simplifies development work. Suppose we have a complex API response:

{
  "code": 0,
  "data": {
    "user_info": {
      "user_name": "phoenix",
      "birth_date": "1990-01-01T00:00:00Z",
      "location": {
        "city": "Beijing",
        "country": "China"
      },
      "height_in_meters": 1.85,
      "is_vip": true,
      "tags": ["tech", null, "swift"],
      "last_login": 1731585275944
    }
  }
}

Using ReerCodable, we can define our model like this:

@Codable
struct ApiResponse {
    var code: Int

    @CodingKey("data.user_info")
    var userInfo: UserInfo
}

@Codable
@SnakeCase
struct UserInfo {
    var userName: String

    @DateCoding(.iso8601)
    var birthDate: Date

    @CodingKey("location.city")
    var city: String

    @CodingKey("location.country")
    var country: String

    @CustomCoding<Double>(
        decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
        encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
    )
    var heightInCentimeters: Double

    var isVip: Bool

    @CompactDecoding
    var tags: [String]

    @DateCoding(.millisecondsSince1970)
    var lastLogin: Date
}

// Usage
do {
    // The original way remains unchanged
    let resp = try JSONDecoder().decode(ApiResponse.self, from: jsonString.data(using: .utf8)!)
    // Convenient method provided by ReerCodable
    let response = try ApiResponse.decode(from: jsonString)
    print("Username: \(response.userInfo.userName)")
    print("Birth date: \(response.userInfo.birthDate)")
    print("Height(cm): \(response.userInfo.heightInCentimeters)")
} catch {
    print("Parsing failed: \(error)")
}

The "Pain" of Native Codable

Before we dive into the magic of ReerCodable, let's review the common pain points when using native Codable:

  1. Manual CodingKeys: When JSON keys don't match property names, you need to manually write the CodingKeys enum. Even if you only modify one property, you have to write all other properties as well. This might be manageable with few properties, but becomes a nightmare once you have many.
  2. Nested Keys: When dealing with deeply nested JSON data, you need to define multiple intermediate structures or manually write decoding logic.
  3. Naming Style Conversion: Backend returns snake_case or kebab-case, while Swift recommends camelCase, requiring manual mapping.
  4. Complex Decoding Logic: If you need custom decoding (type conversion, data fixing, etc.), you have to implement init(from:).
  5. Default Value Handling: For non-Optional properties missing in JSON, an exception will be thrown even if a default value exists. Even Optional enums can fail decoding.
  6. Ignoring Properties: Some properties don't need to participate in encoding/decoding, requiring manual handling in CodingKeys or implementations.
  7. Various Date Formats: Timestamps, ISO8601, custom formats... require configuring different dateDecodingStrategy for JSONDecoder or manual handling.
  8. null in Collections: When arrays or dictionaries contain null values, decoding fails if the corresponding type is non-Optional.
  9. Inheritance: Parent class properties can't be automatically handled in the child class's Codable implementation.
  10. Enum Handling: Enums with associated values or those needing to match multiple raw values have limited support in native Codable.

Community Status

To solve JSON serialization problems, the Swift community has produced many excellent third-party frameworks. Understanding their design philosophies and pros/cons helps us better understand why Swift Macros-based solutions are a better choice today.

1. Frameworks Based on Custom Protocols

ObjectMapper

ObjectMapper is one of the earliest Swift JSON parsing libraries, based on a custom Mappable protocol:

class User: Mappable {
    var name: String?
    var age: Int?

    required init?(map: Map) {}

    func mapping(map: Map) {
        name <- map["user_name"]
        age <- map["user_age"]
    }
}

Features:

  • Not dependent on Swift's native Codable
  • Not dependent on reflection mechanisms
  • Custom operator <- makes mapping code concise
  • Requires manually writing mapping relationships
  • Supports nested mapping and custom conversions

ObjectMapper's advantage is relatively concise code that doesn't depend on Swift's internal implementation details, but its disadvantage is requiring manual mapping code and being incompatible with Swift's native serialization mechanism.

2. Frameworks Based on Runtime Reflection

HandyJSON and KakaJSON

These frameworks adopt similar implementation principles, both obtaining type information through runtime reflection:

struct User: HandyJSON {
    var name: String?
    var age: Int?
}

// Usage
let user = User.deserialize(from: jsonString)

Features:

  • Obtain type metadata through low-level runtime reflection
  • Directly manipulate memory for property assignment
  • Almost no additional code required
  • Relatively high performance

The main problem with these frameworks is their strong dependence on Swift's internal implementation details and metadata structure, making them prone to incompatibility issues or crashes as Swift versions upgrade. They achieve the ideal of "zero code" but sacrifice stability and safety.

3. Frameworks Based on Property Wrappers

ExCodable and BetterCodable

These frameworks leverage property wrappers introduced in Swift 5.1 to extend Codable:

struct User: Codable {
    @CustomKey("user_name")
    var name: String

    @DefaultValue(33)
    var age: Int
}

Features:

  • Based on Swift's native Codable
  • Use property wrappers to simplify common encoding/decoding tasks
  • No need to manually write CodingKeys and Codable implementations
  • Type-safe, compile-time checking

Property wrapper solutions have obvious advantages over the previous two categories: they maintain compatibility with Swift's native Codable while simplifying code writing. However, PropertyWrappers have limited capabilities, and some complex encapsulation designs can't be achieved.

4. Frameworks Based on Macros

CodableWrapper, CodableWrappers, MetaCodable, and this article's ReerCodable

These frameworks leverage macro features introduced in Swift 5.9 to generate Codable implementation code at compile time:

@Codable
struct User {
    @CodingKey("user_name")
    var name: String

    var age: Int = 33
}

Features:

  • Based on Swift's native Codable
  • Declarative syntax, intuitive and easy to understand
  • Highly flexible, supporting complex encoding/decoding logic
  • Can apply macros at the type level

The macro approach combines the advantages of all previous approaches while avoiding their disadvantages: it's based on native Codable, maintaining type safety; it supports declarative syntax, keeping code concise; it generates code at compile time without runtime performance loss; it can handle complex scenarios with strong adaptability.

Why are Macros the Most Elegant Solution?

Among all these frameworks, macro-based solutions (like ReerCodable) provide the most elegant solution for the following reasons:

  1. Seamless Integration with Native Codable: Generated code is identical to handwritten Codable implementations, working perfectly with other APIs using Codable. For modern third-party frameworks like Alamofire, GRDB, etc., they're all compatible with Codable.
  2. Support for Third-party Encoders/Decoders with Codable: If you don't want to use Foundation's decoder, you can use third-party libraries.
  3. Declarative Syntax: Declare serialization requirements through annotations, making code concise, intuitive, and with clear intentions.
  4. Type Safety: All operations undergo type checking at compile time, avoiding runtime errors.
  5. High Flexibility: Can handle various complex scenarios such as nested structures, custom conversions, conditional encoding/decoding, etc.
  6. Good Maintainability: Macro-generated code is predictable and doesn't depend on Swift's internal implementation details, avoiding compatibility issues with Swift version updates.
  7. Strong Debuggability: You can view the expanded code after macro execution, facilitating understanding and debugging.
  8. Extensibility: Different macros can be combined to build complex encoding/decoding logic.

ReerCodable: Magic that Simplifies Complexity

ReerCodable leverages the power of Swift Macros, allowing you to automatically generate efficient, robust Codable implementations by simply adding annotations before types or properties. The core is the @Codable macro, which works with other macros provided by ReerCodable to generate the final encoding/decoding logic. The framework supports both Cocoapods and SwiftPackageManager.

The code implementation references excellent projects like winddpan/CodableWrapper, GottaGetSwifty/CodableWrappers, and MetaCodable, but ReerCodable offers richer features or more concise usage compared to them.

Let's see how ReerCodable elegantly solves the pain points mentioned earlier:

1. Custom CodingKey

Use @CodingKey to specify custom keys for properties without manually writing the CodingKeys enum:

ReerCodable

@Codable
struct User {
    @CodingKey("user_name")
    var name: String

    @CodingKey("user_age")
    var age: Int

    var height: Double
}

Codable

struct User: Codable {
    var name: String
    var age: Int
    var height: Double

    enum CodingKeys: String, CodingKey {
        case name = "user_name"
        case age = "user_age"
        case height
    }
}

2. Nested CodingKey

Support nested key paths through dot notation:

@Codable
struct User {
    @CodingKey("other_info.weight")
    var weight: Double

    @CodingKey("location.city")
    var city: String
}

3. Multiple Keys for Decoding

Multiple keys can be specified for decoding, the system will try them in order until successful:

@Codable
struct User {
    @CodingKey("name", "username", "nick_name")
    var name: String
}

4. Name Style Conversion

Support multiple naming style conversions, can be applied to types or individual properties:

@Codable
@SnakeCase
struct Person {
    var firstName: String  // decoded from "first_name" or encoded to "first_name"

    @KebabCase
    var lastName: String   // decoded from "last-name" or encoded to "last-name"
}

5. Custom Coding Container

Use @CodingContainer to customize the container path for encoding and decoding, typically used when dealing with heavily nested JSON structures while wanting the model declaration to directly match a sub-level structure:

ReerCodable

@Codable
@CodingContainer("data.info")
struct UserInfo {
    var name: String
    var age: Int
}

JSON

{
    "code": 0,
    "data": {
        "info": {
            "name": "phoenix",
            "age": 33
        }
    }
}

6. Encoding-Specific Key

Different key names can be specified for the encoding process. Since @CodingKey may have multiple parameters and can use @SnakeCase, KebabCase, etc., decoding may use multiple keys, then encoding will use the first key, or @EncodingKey can be used to specify the key:

@Codable
struct User {
    @CodingKey("user_name")      // decoding uses "user_name", "name"
    @EncodingKey("name")         // encoding uses "name"
    var name: String
}

7. Default Value Support

Default values can be used when decoding fails. Native Codable throws an exception for non-Optional properties when the correct value is not parsed, even if an initial value has been set, or even if it's an Optional type enum:

@Codable
struct User {
    var age: Int = 33
    var name: String = "phoenix"
    // If the `gender` field in the JSON is neither `male` nor `female`, native Codable will throw an exception, whereas ReerCodable won't and instead set it to nil. For example, with `{"gender": "other"}`, this scenario might occur when the client has defined an enum but the server has added new fields in a business context.
    var gender: Gender?
}

@Codable
enum Gender: String {
    case male, female
}

8. Ignore Properties

Use @CodingIgnored to ignore specific properties during encoding/decoding. During decoding, non-Optional properties must have a default value to satisfy Swift initialization requirements. ReerCodable automatically generates default values for basic data types and collection types. For other custom types, users need to provide default values:

@Codable
struct User {
    var name: String

    @CodingIgnored
    var ignore: Set<String>
}

9. Base64 Coding

Automatically handle conversion between base64 strings and Data, [UInt8] types:

@Codable
struct User {
    @Base64Coding
    var avatar: Data

    @Base64Coding
    var voice: [UInt8]
}

10. Collection Type Decoding Optimization

Use @CompactDecoding to automatically filter null values when decoding arrays, same meaning as compactMap:

@Codable
struct User {
    @CompactDecoding
    var tags: [String]  // ["a", null, "b"] will be decoded as ["a", "b"]
}

At the same time, both Dictionary and Set also support the use of @CompactDecoding for optimization.

11. Date Coding

Support various date format encoding/decoding:

@Codable
class DateModel {
    @DateCoding(.timeIntervalSince2001)
    var date1: Date

    @DateCoding(.timeIntervalSince1970)
    var date2: Date

    @DateCoding(.secondsSince1970)
    var date3: Date

    @DateCoding(.millisecondsSince1970)
    var date4: Date

    @DateCoding(.iso8601)
    var date5: Date

    @DateCoding(.formatted(Self.formatter))
    var date6: Date

    static let formatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
        dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        return dateFormatter
    }()
}

JSON

{
    "date1": 1431585275,
    "date2": 1731585275.944,
    "date3": 1731585275,
    "date4": 1731585275944,
    "date5": "2024-12-10T00:00:00Z",
    "date6": "2024-12-10T00:00:00.000"
}

12. Custom Encoding/Decoding Logic

Implement custom encoding/decoding logic through @CustomCoding. There are two ways to customize encoding/decoding:

  • Through closures, using decoder: Decoder, encoder: Encoder as parameters to implement custom logic:
@Codable
struct User {
    @CustomCoding<Double>(
        decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
        encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
    )
    var heightInCentimeters: Double
}
  • Through a custom type implementing the CodingCustomizable protocol to implement custom logic:
// 1st 2nd 3rd 4th 5th  -> 1 2 3 4 5
struct RankTransformer: CodingCustomizable {

    typealias Value = UInt

    static func decode(by decoder: any Decoder, keys: [String]) throws -> UInt {
        var temp: String = try decoder.value(forKeys: keys)
        temp.removeLast(2)
        return UInt(temp) ?? 0
    }

    static func encode(by encoder: Encoder, key: String, value: Value) throws {
        try encoder.set(value, forKey: key)
    }
}

@Codable
struct HundredMeterRace {
    @CustomCoding(RankTransformer.self)
    var rank: UInt
}

During custom implementation, the framework provides methods that can make encoding/decoding more convenient:

public extension Decoder {
    func value<Value: Decodable>(forKeys keys: String...) throws -> Value {
        let container = try container(keyedBy: AnyCodingKey.self)
        return try container.decode(type: Value.self, keys: keys)
    }
}

public extension Encoder {
    func set<Value: Encodable>(_ value: Value, forKey key: String, treatDotAsNested: Bool = true) throws {
        var container = container(keyedBy: AnyCodingKey.self)
        try container.encode(value: value, key: key, treatDotAsNested: treatDotAsNested)
    }
}

13. Inheritance Support

Use @InheritedCodable for better support of subclass encoding/decoding. Native Codable cannot parse subclass properties, even if the value exists in JSON, requiring manual implementation of init(from decoder: Decoder) throws:

@Codable
class Animal {
    var name: String
}

@InheritedCodable
class Cat: Animal {
    var color: String
}

14. Enum Support

Provide rich encoding/decoding capabilities for enums:

  • Support for basic enum types and RawValue enums:
@Codable
struct User {
    let gender: Gender
    let rawInt: RawInt
    let rawDouble: RawDouble
    let rawDouble2: RawDouble2
    let rawString: RawString
}

@Codable
enum Gender {
    case male, female
}

@Codable
enum RawInt: Int {
    case one = 1, two, three, other = 100
}

@Codable
enum RawDouble: Double {
    case one, two, three, other = 100.0
}

@Codable
enum RawDouble2: Double {
    case one = 1.1, two = 2.2, three = 3.3, other = 4.4
}

@Codable
enum RawString: String {
    case one, two, three, other = "helloworld"
}
  • Support using CodingCase(match: ....) to match multiple values or ranges:
@Codable
enum Phone: Codable {
    @CodingCase(match: .bool(true), .int(10), .string("iphone"), .intRange(22...30))
    case iPhone

    @CodingCase(match: .int(12), .string("MI"), .string("xiaomi"), .doubleRange(50...60))
    case xiaomi

    @CodingCase(match: .bool(false), .string("oppo"), .stringRange("o"..."q"))
    case oppo
}
  • For enums with associated values, support using AssociatedValue to match associated values, use .label() to declare matching logic for labeled associated values, use .index() to declare matching logic for unlabeled associated values. ReerCodable supports two JSON formats for enum matching:

    • The first is also supported by native Codable, where the enum value and its associated values have a parent-child structure:
    @Codable
    enum Video: Codable {
        /// {
        ///     "YOUTUBE": {
        ///         "id": "ujOc3a7Hav0",
        ///         "_1": 44.5
        ///     }
        /// }
        @CodingCase(match: .string("youtube"), .string("YOUTUBE"))
        case youTube
    
        /// {
        ///     "vimeo": {
        ///         "ID": "234961067",
        ///         "minutes": 999999
        ///     }
        /// }
        @CodingCase(
            match: .string("vimeo"),
            values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")]
        )
        case vimeo(id: String, duration: TimeInterval = 33, Int)
    
        /// {
        ///     "tiktok": {
        ///         "url": "https://example.com/video.mp4",
        ///         "tag": "Art"
        ///     }
        /// }
        @CodingCase(
            match: .string("tiktok"),
            values: [.label("url", keys: "url")]
        )
        case tiktok(url: URL, tag: String?)
    }
    
    • The second is where enum values and their associated values are at the same level or have custom matching structures, using CaseMatcher with key path for custom path value matching:
    @Codable
    enum Video1: Codable {
        /// {
        ///     "type": {
        ///         "middle": "youtube"
        ///     }
        /// }
        @CodingCase(match: .string("youtube", at: "type.middle"))
        case youTube
    
        /// {
        ///     "type": "vimeo",
        ///     "ID": "234961067",
        ///     "minutes": 999999
        /// }
        @CodingCase(
            match: .string("vimeo", at: "type"),
            values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")]
        )
        case vimeo(id: String, duration: TimeInterval = 33, Int)
    
        /// {
        ///     "type": "tiktok",
        ///     "media": "https://example.com/video.mp4",
        ///     "tag": "Art"
        /// }
        @CodingCase(
            match: .string("tiktok", at: "type"),
            values: [.label("url", keys: "media")]
        )
        case tiktok(url: URL, tag: String?)
    }
    

15. Lifecycle Callbacks

Support encoding/decoding lifecycle callbacks:

@Codable
class User {
    var age: Int

    func didDecode(from decoder: any Decoder) throws {
        if age < 0 {
            throw ReerCodableError(text: "Invalid age")
        }
    }

    func willEncode(to encoder: any Encoder) throws {
        // Process before encoding
    }
}

@Codable
struct Child: Equatable {
    var name: String

    mutating func didDecode(from decoder: any Decoder) throws {
        name = "reer"
    }

    func willEncode(to encoder: any Encoder) throws {
        print(name)
    }
}

16. JSON Extension Support

Provide convenient JSON string and dictionary conversion methods:

let jsonString = "{\"name\": \"Tom\"}"
let user = try User.decode(from: jsonString)

let dict: [String: Any] = ["name": "Tom"]
let user2 = try User.decode(from: dict)

17. Basic Type Conversion

Support automatic conversion between basic data types:

@Codable
struct User {
    @CodingKey("is_vip")
    var isVIP: Bool    // "1" or 1 can be decoded as true

    @CodingKey("score")
    var score: Double  // "100" or 100 can be decoded as 100.0
}

18. AnyCodable Support

Implement encoding/decoding of Any type through AnyCodable:

@Codable
struct Response {
    var data: AnyCodable  // Can store data of any type
    var metadata: [String: AnyCodable]  // Equivalent to [String: Any] type
}

19. Generate Default Instance

@Codable
@DefaultInstance
struct ImageModel {
    var url: URL
}

@Codable
@DefaultInstance
struct User5 {
    let name: String
    var age: Int = 22
    var uInt: UInt = 3
    var data: Data
    var date: Date
    var decimal: Decimal = 8
    var uuid: UUID
    var avatar: ImageModel
    var optional: String? = "123"
    var optional2: String?
}

Will generate the following instance:

static let `default` = User5(
    name: "",
    age: 22,
    uInt: 3,
    data: Data(),
    date: Date(),
    decimal: 8,
    uuid: UUID(),
    avatar: ImageModel.default,
    optional: "123",
    optional2: nil
)

⚠️ Note: Properties with generic types are NOT supported with @DefaultInstance:

@Codable
struct NetResponse<Element: Codable> {
    let data: Element?
    let msg: String
    private(set) var code: Int = 0
}

20. Generate Copy Method

Use Copyable to generate copy method for models:

@Codable
@Copyable
public struct Model6 {
    var name: String
    let id: Int
    var desc: String?
}

@Codable
@Copyable
class Model7<Element: Codable> {
    var name: String
    let id: Int
    var desc: String?
    var data: Element?
}

Generates the following copy methods. As you can see, besides default copy, you can also update specific properties:

public func copy(
    name: String? = nil,
    id: Int? = nil,
    desc: String? = nil
) -> Model6 {
    return .init(
        name: name ?? self.name,
        id: id ?? self.id,
        desc: desc ?? self.desc
    )
}

func copy(
    name: String? = nil,
    id: Int? = nil,
    desc: String? = nil,
    data: Element? = nil
) -> Model7 {
    return .init(
        name: name ?? self.name,
        id: id ?? self.id,
        desc: desc ?? self.desc,
        data: data ?? self.data
    )
}

21. Use @Decodable or @Encodable alone

@Decodable
struct Item: Equatable {
    let id: Int
}

@Encodable
struct User3: Equatable {
    let name: String
}

These examples demonstrate the main features of ReerCodable, which can help developers greatly simplify the encoding/decoding process, improving code readability and maintainability.

About Performance

ReerCodable theoretically has the same performance as native Codable, but early Foundation JSONDecoder performance wasn't good, so the community created the following frameworks:

  • ZippyJSON (Implemented Decoder, Encoder in C++)
  • Ananda (Based on yyjson)
  • IkigaJSON (Implemented Decoder, Encoder in Swift)

From ZippyJSON's homepage description, it seems Apple optimized decoding performance in iOS17+ and surpassed ZippyJSON:

Note: JSONDecoder is faster than ZippyJSON for iOS 17+. The rest of this document describes the performance difference pre-iOS 17.

I used ReerCodable to annotate models and their properties, comparing the decoding performance of Foundation JSONDecoder, ZippyJSON, and IkigaJSON:

name                                     time           std        iterations
-----------------------------------------------------------------------------
JSON解码性能对比.创建 Foundation JSONDecoder          83.000 ns ±  95.19 %    1000000
JSON解码性能对比.创建 IkigaJSONDecoder                41.000 ns ±  96.06 %    1000000
JSON解码性能对比.创建 ZippyJSONDecoder                41.000 ns ±  77.25 %    1000000

JSON解码性能对比.Foundation JSONDecoder - 标准数据  313791.000 ns ±   3.63 %       4416
JSON解码性能对比.IkigaJSONDecoder - 标准数据        377583.000 ns ±   6.30 %       3692
JSON解码性能对比.ZippyJSONDecoder - 标准数据        310792.000 ns ±   3.62 %       4395

JSON解码性能对比.Foundation JSONDecoder - 小数据集   88334.000 ns ±   4.35 %      15706
JSON解码性能对比.IkigaJSONDecoder - 小数据集         98333.000 ns ±   4.96 %      14095
JSON解码性能对比.ZippyJSONDecoder - 小数据集         87625.000 ns ±   5.34 %      15747

JSON解码性能对比.Foundation JSONDecoder - 大数据集 5537916.500 ns ±   1.61 %        252
JSON解码性能对比.IkigaJSONDecoder - 大数据集       6445166.000 ns ±   2.30 %        217
JSON解码性能对比.ZippyJSONDecoder - 大数据集       5376375.000 ns ±   1.68 %        259

JSON解码性能对比.Foundation JSONDecoder - 嵌套结构    9167.000 ns ±   8.38 %     149385
JSON解码性能对比.IkigaJSONDecoder - 嵌套结构         10375.000 ns ±  13.73 %     131397
JSON解码性能对比.ZippyJSONDecoder - 嵌套结构          8458.000 ns ±  10.45 %     161606

JSON解码性能对比.Foundation JSONDecoder - 数组解析 2562250.000 ns ±   2.08 %        542
JSON解码性能对比.IkigaJSONDecoder - 数组解析       3620500.000 ns ±   1.63 %        385
JSON解码性能对比.ZippyJSONDecoder - 数组解析       2503709.000 ns ±   1.94 %        555

As seen above, ZippyJSONDecoder still has the best performance, with Foundation JSONDecoder very close behind, much better than older versions of JSONDecoder.

Additionally, native Foundation.JSONDecoder decoding performance still hasn't surpassed Ananda, according to AnandaBenchmark. The following benchmark data shows Ananda's performance is about twice that of Foundation.JSONDecoder, so if you have very high performance requirements, consider using Ananda. It also uses Swift macros for some convenient encapsulation, but generally speaking, native Codable no longer has performance issues:

name                       time        std        iterations
------------------------------------------------------------
Codable decoding           5125.000 ns ±  14.92 %     271764
Ananda decoding            2541.000 ns ±  38.26 %     541187
Ananda decoding with Macro 2541.000 ns ±  64.55 %     550339

Conclusion

ReerCodable greatly simplifies the use of Codable through a series of carefully designed Swift Macros, significantly reducing boilerplate code and improving development efficiency and code readability. It not only covers most scenarios of native Codable but also provides more powerful and flexible features such as multi-key decoding, name conversion, custom containers, robust default value handling, powerful enum support, and convenient auxiliary tools.

If you're still troubled by the tedious implementation of Codable, try ReerCodable, and it will surprise you!

GitHub address: https://github.com/reers/ReerCodable

Welcome to try it out, star the repository, submit issues or pull requests! Let's write Swift code in a more modern and elegant way together!

This article was mainly generated by AI, please refer to the GitHub readme for specifics.