macro-codable-kit

0.3.0

Efficient, flexible Codable with Swift Macros
mikhailmaslo/macro-codable-kit

What's New

0.3.0

2023-10-16T23:09:39Z

What's Changed

  • Use macro api to get list conformances to generate by @mikhailmaslo in #4
  • Lint unused code with periphery by @mikhailmaslo in #5 & #6
  • Remove Codable constraint on ValueCodableStrategy.Value by @mikhailmaslo in #7
  • Add comprehensive docc documentation. Added tutorial to implement your own CustomCoding by @mikhailmaslo in #8

Full Changelog: 0.2.0...0.3.0

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

  • You can't implement things like OneOf or AllOf 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 use let and you'll probably endup with private(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.

Features

  • 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 with CustomCodingEncoding.errorHandler.
  • Zero additional allocations: Coding is done by predefined clean (static) functions, so there's no need to allocate additional memory

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

  • BoolTrue - true by default, BoolFalse - false by default
  • IntZero - Int(0) by default
  • DoubleZero - Double(0) by default

@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

  • 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 or Double to Date, example: 1696291200.0
  • Misc:
    • Base64Data - converts base64 string to Data

@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

  • swift-foundation by Apple Swift Foundation Team provided a profound understanding of Decoder, Encoder, and the handling of encodeIfPresent and decodeIfPresent 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 for SafeDecoding on dictionaries

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

Last updated: Fri Dec 06 2024 02:42:59 GMT-1000 (Hawaii-Aleutian Standard Time)