Home

Awesome

MacroCodableKit

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.

Table of Contents

Motivation

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

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.

Features

... and more!

Usage

Basics - @Codable

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

@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

@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 ... } }

Annotations

@CodingKey

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
}

@OmitCoding

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
}

@DefaultValue

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:

@ValueCodable

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

@CustomCoding

@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.

Installation

Swift Package Manager

Known Limitations

Acknowledgments