MacroCodableKit enhances your Codable experience in Swift, leveraging macros to generate precise and efficient code with zero additional memory allocations, thanks to the usage of pure (static) functions. It's a comprehensive solution providing support for AllOf, OneOf, and customizable CodingKeys, extending the native Codable capabilities to keep up with OpenAPI specification seamlessly.
- MacroCodableKit
- Table of Contents
- Motivation
- Features
- Usage
- Installation
- Known Limitations
- Acknowledgments
Codable with property wrappers is an alternative way to approach Codable, it's quite a flexible approach and surely greatly reduces amount of boilerplate, at the same time it has its limitations
- You can't implement things like
OneOforAllOffrom OpenAPI which is quite common in nowdays APIs - You can't alter
CodingKeyswith@propertyWrapper @propertyWrapperis an additional struct next to a property, so you might endup with twice as much allocations then needed@propertyWrappershould be settable, so you can't useletand you'll probably endup withprivate(set)
There is a proposal though to allow
letfor@propertyWrapperswhich I hope will be eventually completed
Approaching Codable with Macros allows zero additional allocation, immutable properties, and the implementation of any desirable Codable strategy inside one tool - MacroCodableKit, which provides OneOf and AllOf coding implementations from OpenAPI spec, and allows CodingKeys altering via annotations.
- OpenAPI Compatibility: Embrace OpenAPI specifications with @OneOfCodable, @AllOfCodable with ease.
- Custom Coding Keys: Specify custom coding keys with
CodingKey(_ key: String)annotation. - Skip Coding of a Property: Opt to ignore certain properties from being coded with
OmitCoding. - Default Values and Strategies: Use @DefaultValue to handle failed decoding gracefully or @ValueStrategy to decode values your way, such as handling dates in various formats.
- Custom Coding Strategies: Utilize @CustomCoding to define your encoding and decoding logic. Use build-in
SafeDecodingto handle arrays and dictionaries in a safe manner. - Error Handling: Handle ignored decoding errors with
CustomCodingDecoding.errorHandler& encoding errors withCustomCodingEncoding.errorHandler. - Zero additional allocations: Coding is done by predefined clean (static) functions, so there's no need to allocate additional memory
... and more!
Annotate a struct with @Codable, without additional annotations on properties it will generate default Codable conformance
Note Do not conform to
Codableprotocol yourself, it will prevent macro from generating code
@Codable
struct User {
let birthday: Double
let name: String
let isVerified: Bool
}Let's convert birthday to Date, change coding key of isVerified and make it default to false
Note Conform only to
@Decodableif you don't need encoding
@Decodable
struct User {
@ValueStrategy(ISO8601Default)
let birthday: Date
let name: String
@CodingKey("is_verified")
@DefaultValue(BoolFalse)
let isVerified: Bool
}
// json: { "birthday": 1696291200.0, "name": "Mikhail" }
// is_verified is not specified, so the default value is "false" as specified by `@DefaultValue`@AllOfCodable describes OpenAPI AllOf relationship
Imagine you have SocialUser OpenAPI specification which inherits from User and have additional properties
SocialUser:
allOf:
- $ref: '#components/schema/User'
- type: object
properties:
username:
type: string
isPublic:
type: booleanIn Swift code it could be implemented with just AllOfCodable annotation
@AllOfCodable
struct SocialUser {
struct Properties: Codable {
let isPublic: Bool
let username: String
}
let user: User
let additionalProperties: Properties
}@OneOfCodable describes OpenAPI OneOf relationship
Note Only one associated value is expected in each enum case
@OneOfCodable
enum PaymentMethod {
case card(DebitCardPayload)
case applePay(payload: ApplePayPayload)
case sepa(_ payload: SepaPayload)
}
// valid jsons:
// { "card": { ... DebitCardPayload ... }
// { "applePay": { ... ApplePayPayload ... } }
// { "sepa": { ... SepaPayload ... } }Annotate a property with @CodingKey(_ key: String), key will be used as CodingKey in decoding and encoding
struct User {
@CodingKey("is_verified")
let isVerified: Bool
}Skip coding for a specific property with @OmitCoding() annotation
struct User {
@OmitCoding()
let isVerified: Bool
}It might be useful when you describe an object, where each encoded property is a part of a http request body
@Encodable
struct Request {
var endpoint: String { "/user/\(userID)/follow" }
// We don't want to encode userID, since it's not part of the request body
@OmitCoding
let userID: String
let isFollowing: Bool
}Use @DefaultValue<Provider: DefaultValueProvider>(_ type: Provider.Type) to provide default value if decoding fails
Warning
@DefaultValue(_:)doesn't affect encoding
@Codable
struct User {
@DefaultValue(BoolFalse) // property will be `false`, if value is absent or decoding fails
let isVerified: Bool
}Build-in presets:
- BoolTrue - true by default, BoolFalse - false by default
- IntZero - Int(0) by default
- DoubleZero - Double(0) by default
Use @ValueStrategy<Strategy: ValueCodableStrategy>(_ strategy: Strategy.Type) to provide custom mapping
@Encodable
struct Upload {
@ValueStrategy(Base64Data)
let document: Data
}Can be combined with DefaultValue
@Decodable
struct Example {
@ValueStrategy(SomeStringStrategy)
@DefaultValue(EmptyString)
let string: String
}Build-in presets
- Dates:
- ISO8601Default - handles dates in ISO8601 format, for example,
2023-10-03T10:15:30+00:00 - ISO8601WithFullDate - handles dates with the full date in ISO8601 format, for example,
2023-10-03 - ISO8601WithFractionalSeconds - handles dates with fractional seconds in ISO8601 format, for example,
2023-10-03T10:15:30.123+00:00 - YearMonthDayDate - handles dates with full date, example:
2023-10-03 - RFC2822Date - handles dates in "EEE, d MMM y HH:mm:ss zzz" (RFC2822), example:
Tue, 3 Oct 2023 10:15:30 GMT - RFC3339Date - handles dates in "yyyy-MM-dd'T'HH:mm:ssZ" (RFC2822), example:
2023-10-03T10:15:30Z - TimestampedDate - converts timestamp either
StringorDoubletoDate, example:1696291200.0
- ISO8601Default - handles dates in ISO8601 format, for example,
- Misc:
- Base64Data - converts base64 string to Data
@CustomCoding annotation allows specifying custom encoding and decoding strategies for properties. Attach it to a property and specify a type that contains the custom encoding/decoding logic. For instance, @CustomCoding(SafeDecoding) uses safeDecoding functions from CustomCodingDecoding and CustomCodingEncoding for handling arrays and dictionaries safely during encoding and decoding.
@Codable
struct TaggedPhotos: Equatable {
@CustomCoding(SafeDecoding)
var photos: [Photo]
}
@Codable
struct UserProfiles: Equatable {
@CustomCoding(SafeDecoding)
var profiles: [String: Profile]
}
// Corresponding JSON for TaggedPhotos with corrupted data
// {
// "photos": [
// { "url": "https://example.com/photo1.jpg", "tag": "vacation" },
// { "url": "https://example.com/photo2.jpg", "tag": "family" },
// "corruptedData"
// ]
// }
// Corresponding JSON for UserProfiles with corrupted data
// {
// "profiles": {
// "john_doe": { "age": 25, "location": "NYC" },
// "jane_doe": { "age": 28, "location": "LA" },
// "corrupted_entry": "corruptedData"
// }
// }In the example above, @CustomCoding(SafeDecoding) will catch and forward any decoding errors caused by invalid decoding to CustomCodingDecoding.errorHandler, allowing the rest of the data to be decoded safely.
- @Codable, @AllOfCodable are only applicable to
struct
-
swift-foundation by Apple Swift Foundation Team provided a profound understanding of
Decoder,Encoder, and the handling ofencodeIfPresentanddecodeIfPresentfunctions which logic is required for the library such as MacroCodableKit. -
BetterCodable by Mark Sands: This project has all the common and widespread use cases for
Codable, which was adopted in MacroCodableKit. Especially key conversion case which turned out to be vital forSafeDecodingon dictionaries