https://github.com/reers/ReerCodable
Codable extensions using Swift Macro
https://github.com/reers/ReerCodable
ananda bettercodable codable codablewrapper codablewrappers decode encode jsonparser jsonserializer jsontomodel kakajson metacodable mjextension objectmapper reercodable swiftmacro yymodel
Last synced: 6 months ago
JSON representation
Codable extensions using Swift Macro
- Host: GitHub
- URL: https://github.com/reers/ReerCodable
- Owner: reers
- License: mit
- Created: 2024-10-15T03:48:03.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-04-11T02:05:29.000Z (7 months ago)
- Last Synced: 2025-04-11T03:20:10.621Z (7 months ago)
- Topics: ananda, bettercodable, codable, codablewrapper, codablewrappers, decode, encode, jsonparser, jsonserializer, jsontomodel, kakajson, metacodable, mjextension, objectmapper, reercodable, swiftmacro, yymodel
- Language: Swift
- Homepage:
- Size: 11.7 MB
- Stars: 138
- Watchers: 2
- Forks: 3
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-swift - ReerCodable - Codable extensions using Swift macro. (Libs / Data Management)
- fucking-awesome-swift - ReerCodable - Codable extensions using Swift macro. (Libs / Data Management)
README
[简体中文](README_CN.md)
# ReerCodable
Extension of `Codable` using Swift macros to make serialization simpler with declarative annotations!
```swift
@Codable
@SnakeCase
struct User {
@CodingKey("user_name")
var name: String
@KebabCase
@DateCoding(.iso8601)
var birthDate: Date
@CodingKey("location.city")
var city: 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 height: Double
}
```
# Overview
ReerCodable framework provides a series of custom macros for generating dynamic Codable implementations. The core of the framework is the @Codable() macro, which generates concrete implementations under data annotations provided by other macros (⚠️ Only the `@Codable` macro can be expanded in XCode macro expansion, expanding other macros will have no response)
The framework has been fully tested using [Swift Testing](https://developer.apple.com/xcode/swift-testing/).
Main features include:
- Declare custom `CodingKey` values for each property through `@CodingKey("key")`, without writing all `CodingKey` values.
- Support nested `CodingKey` through string expressions, like `@CodingKey("nested.key")`
- Allow using multiple `CodingKey`s for Decode, like `@CodingKey("key1", "key2")`
- Support using `@SnakeCase`, `KebabCase` etc. to mark types or properties for easy naming conversion
- Customize nested containers during Coding using `@CodingContainer`
- Support specified `CodingKey` for Encode, like `EncodingKey("encode_key")`
- Allow using default values when decoding fails to avoid `keyNotFound` errors
- Allow using `@CodingIgnored` to ignore specific properties during encoding/decoding
- Support automatic conversion between base64 strings and `Data` `[UInt8]` types using `@Base64Coding`
- Through `@CompactDecoding`, ignore `null` values when Decoding `Array`, `Dictionary`, `Set` instead of throwing errors
- Support various encoding/decoding of `Date` through `@DateCoding`
- Support custom encoding/decoding logic through `@CustomCoding`
- Better support for subclasses using `@InheritedCodable`
- Provide simple and rich encoding/decoding capabilities for various `enum` types
- Support encoding/decoding lifecycle through `ReerCodableDelegate`, like `didDecode`, `willEncode`
- Provide extensions to support using JSON String, `Dictionary`, `Array` directly as parameters for encoding/decoding
- Support conversion between basic data types like `Bool`, `String`, `Double`, `Int`, `CGFloat`
- Support BigInt `Int128`, `UInt128` on macOS 15+, iOS 13+
- Support encoding/decoding of `Any` through `AnyCodable`, like `var dict = [String: AnyCodable]`
- Auto-generate default instances:
Use `@DefaultInstance` to automatically create a default instance of your type,
accessible through `Model.default`
- Flexible copying with updates:
The `@Copyable` macro generates a powerful `copy()` method that allows both
full copies and selective property updates in a single call
# Requirements
XCode 16.0+
iOS 13.0+, macOS 10.15+, tvOS 13.0+, visionOS 1.0+, watchOS 6.0+
Swift 5.10+
swift-syntax 600.0.0+
# Installation
Swift Package Manager
You can install ReerCodable using The Swift Package Manager by adding the proper description to your Package.swift file:
import PackageDescription
let package = Package(
name: "YOUR_PROJECT_NAME",
targets: [],
dependencies: [
.package(url: "https://github.com/reers/ReerCodable.git", from: "1.1.7")
]
)
Then, add ReerCodable to your targets dependencies like so:
.product(name: "ReerCodable", package: "ReerCodable"),
Finally, run swift package update.
CocoaPods
Since CocoaPods doesn't directly support Swift Macro, the macro implementation can be compiled into binary for use. The integration method is as follows, requiring s.pod_target_xcconfig to load the binary plugin of macro implementation:
Pod::Spec.new do |s|
s.name = 'YourPod'
s.dependency 'ReerCodable', '1.1.7'
# Copy the following config to your pod
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_ROOT}/ReerCodable/Sources/Resources/ReerCodableMacros#ReerCodableMacros'
}
end
Alternatively, if not using s.pod_target_xcconfig and s.user_target_xcconfig, you can add the following script in podfile for unified processing:
post_install do |installer|
installer.pods_project.targets.each do |target|
rhea_dependency = target.dependencies.find { |d| ['ReerCodable'].include?(d.name) }
if rhea_dependency
puts "Adding ReerCodable Swift flags to target: #{target.name}"
target.build_configurations.each do |config|
swift_flags = config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)']
plugin_flag = '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_ROOT}/ReerCodable/Sources/Resources/ReerCodableMacros#ReerCodableMacros'
unless swift_flags.join(' ').include?(plugin_flag)
swift_flags.concat(plugin_flag.split)
end
config.build_settings['OTHER_SWIFT_FLAGS'] = swift_flags
end
end
end
end
# Usage
ReerCodable greatly simplifies Swift's serialization process through declarative annotations. Here are detailed examples of each feature:
### 1. Custom CodingKey
Use `@CodingKey` to specify custom keys for properties without manually writing `CodingKeys` enum:
ReerCodable
Codable
```swift
@Codable
struct User {
@CodingKey("user_name")
var name: String
@CodingKey("user_age")
var age: Int
var height: Double
}
```
```swift
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 using dot notation:
```swift
@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 decoding in order until successful:
```swift
@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:
```swift
@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
JSON
```swift
@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
```swift
@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
```swift
@Codable
struct User {
var age: Int = 33
var name: String = "phoenix"
// If the `gender` field in the JSON is neither `male` nor `female`, the native Codable will throw an exception, whereas ReerCodable will not 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.
```swift
@Codable
struct User {
var name: String
@CodingIgnored
var ignore: Set
}
```
### 9. Base64 Coding
Automatically handle conversion between base64 strings and `Data`, `[UInt8]` types:
```swift
@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`:
```swift
@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:
ReerCodable
JSON
```swift
@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:
```swift
@Codable
struct User {
@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
}
```
- Through a custom type implementing the `CodingCustomizable` protocol to implement custom logic:
```swift
// 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:
```swift
public extension Decoder {
func value(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: 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`
```swift
@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
```swift
@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
```swift
@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 `CaseValue` 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:
```swift
@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 `.pathValue()` for custom path value matching
```swift
@Codable
enum Video1: Codable {
/// {
/// "type": {
/// "middle": "youtube"
/// }
/// }
@CodingCase(match: .pathValue("type.middle.youtube"))
case youTube
/// {
/// "type": "vimeo",
/// "ID": "234961067",
/// "minutes": 999999
/// }
@CodingCase(
match: .pathValue("type.vimeo"),
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: .pathValue("type.tiktok"),
values: [.label("url", keys: "media")]
)
case tiktok(url: URL, tag: String?)
}
```
### 15. Lifecycle Callbacks
Support encoding/decoding lifecycle callbacks:
```swift
@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:
```swift
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:
```swift
@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`:
```swift
@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
```swift
@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:
```swift
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`
```swift
@Codable
struct NetResponse {
let data: Element?
let msg: String
private(set) var code: Int = 0
}
```
### 20. Generate Copy Method
Use `Copyable` to generate `copy` method for models
```swift
@Codable
@Copyable
public struct Model6 {
var name: String
let id: Int
var desc: String?
}
@Codable
@Copyable
class Model7 {
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:
```swift
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
)
}
```
These examples demonstrate the main features of ReerCodable, which can help developers greatly simplify the encoding/decoding process, improving code readability and maintainability.