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

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
:
-
Manual
CodingKeys
: When JSON keys don't match property names, you need to manually write theCodingKeys
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
forJSONDecoder
or manual handling. -
null
in Collections: When arrays or dictionaries containnull
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 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:
- 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.
- Support for Third-party Encoders/Decoders with Codable: If you don't want to use Foundation's decoder, you can use third-party libraries.
- Declarative Syntax: Declare serialization requirements through annotations, making code concise, intuitive, and with clear intentions.
- Type Safety: All operations undergo type checking at compile time, avoiding runtime errors.
- High Flexibility: Can handle various complex scenarios such as nested structures, custom conversions, conditional encoding/decoding, etc.
- Good Maintainability: Macro-generated code is predictable and doesn't depend on Swift's internal implementation details, avoiding compatibility issues with Swift version updates.
- Strong Debuggability: You can view the expanded code after macro execution, facilitating understanding and debugging.
- 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?) }
- The first is also supported by native
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.