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
OneOf
orAllOf
from OpenAPI which is quite common in nowdays APIs - You can't alter
CodingKeys
with@propertyWrapper
@propertyWrapper
is an additional struct next to a property, so you might endup with twice as much allocations then needed@propertyWrapper
should be settable, so you can't uselet
and you'll probably endup withprivate(set)
There is a proposal though to allow
let
for@propertyWrappers
which 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
SafeDecoding
to 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
Codable
protocol 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
@Decodable
if 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: boolean
In 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
String
orDouble
toDate
, 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 ofencodeIfPresent
anddecodeIfPresent
functions 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 forSafeDecoding
on dictionaries