/MetaCodable

Supercharge Swift's Codable implementations with macros meta-programming.

Primary LanguageSwiftMIT LicenseMIT

MetaCodable

API Docs Swift Package Manager Compatible CocoaPods Compatible Swift Platforms CI/CD CodeFactor codecov

Supercharge Swift's Codable implementations with macros.

Overview

MetaCodable framework exposes custom macros which can be used to generate dynamic Codable implementations. The core of the framework is Codable() macro which generates the implementation aided by data provided with using other macros.

MetaCodable aims to supercharge your Codable implementations by providing these inbox features:

  • Allows custom CodingKey value declaration per variable with CodedAt(_:) passing single argument, instead of requiring you to write all the CodingKey values.
  • Allows to create flattened model for nested CodingKey values with CodedAt(_:) and CodedIn(_:).
  • Allows to create composition of multiple Codable types with CodedAt(_:) passing no arguments.
  • Allows to read data from additional fallback CodingKeys provided with CodedAs(_:_:).
  • Allows to provide default value in case of decoding failures with Default(_:), or only in case of failures when missing value with Default(ifMissing:). Different default values can also be used for value missing and other errors respectively with Default(ifMissing:forErrors:).
  • Allows to create custom decoding/encoding strategies with HelperCoder and using them with CodedBy(_:). i.e. LossySequenceCoder etc.
  • Allows specifying different case values with CodedAs(_:_:) and case value/protocol type identifier type different from String with CodedAs().
  • Allows specifying enum-case/protocol type identifier path with CodedAt(_:) and case content path with ContentAt(_:_:).
  • Allows decoding/encoding enums that lack distinct identifiers for each case data with UnTagged().
  • Allows to ignore specific properties/cases from decoding/encoding with IgnoreCoding(), IgnoreDecoding() and IgnoreEncoding(). Allows to ignore encoding based on custom conditions with IgnoreEncoding(if:).
  • Allows to use camel-case names for variables according to Swift API Design Guidelines, while enabling a type/case to work with different case style keys with CodingKeys(_:).
  • Allows to ignore all initialized properties of a type/case from decoding/encoding with IgnoreCodingInitialized() unless explicitly asked to decode/encode by attaching any coding attributes, i.e. CodedIn(_:), CodedAt(_:), CodedBy(_:), Default(_:) etc.
  • Allows to generate protocol decoding/encoding HelperCoders with MetaProtocolCodable build tool plugin from DynamicCodable types.

See the limitations for this macro.

Requirements

Platform Minimum Swift Version Installation Status
iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ 5.9 Swift Package Manager, CocoaPods Fully Tested
Linux 5.9 Swift Package Manager Fully Tested
Windows 5.9.1 Swift Package Manager Fully Tested

Installation

Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler.

Once you have your Swift package set up, adding MetaCodable as a dependency is as easy as adding it to the dependencies value of your Package.swift.

.package(url: "https://github.com/SwiftyLab/MetaCodable.git", from: "1.0.0"),

Then you can add the MetaCodable module product as dependency to the targets of your choosing, by adding it to the dependencies value of your targets.

.product(name: "MetaCodable", package: "MetaCodable"),

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate MetaCodable into your Xcode project using CocoaPods, specify it in your Podfile:

pod 'MetaCodable'

Usage

MetaCodable allows to get rid of boiler plate that was often needed in some typical Codable implementations with features like:

Custom `CodingKey` value declaration per variable, instead of requiring you to write for all fields.

i.e. in the official docs, to define custom CodingKey for 2 fields of Landmark type you had to write:

struct Landmark: Codable {
    var name: String
    var foundingYear: Int
    var location: Coordinate
    var vantagePoints: [Coordinate]

    enum CodingKeys: String, CodingKey {
        case name = "title"
        case foundingYear = "founding_date"
        case location
        case vantagePoints
    }
}

But with MetaCodable all you have to write is this:

@Codable
struct Landmark {
    @CodedAt("title")
    var name: String
    @CodedAt("founding_date")
    var foundingYear: Int

    var location: Coordinate
    var vantagePoints: [Coordinate]
}
Create flattened model for nested `CodingKey` values.

i.e. in official docs to decode a JSON like this:

{
  "latitude": 0,
  "longitude": 0,
  "additionalInfo": {
      "elevation": 0
  }
}

You had to write all these boilerplate:

struct Coordinate {
    var latitude: Double
    var longitude: Double
    var elevation: Double

    enum CodingKeys: String, CodingKey {
        case latitude
        case longitude
        case additionalInfo
    }

    enum AdditionalInfoKeys: String, CodingKey {
        case elevation
    }
}

extension Coordinate: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        latitude = try values.decode(Double.self, forKey: .latitude)
        longitude = try values.decode(Double.self, forKey: .longitude)

        let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
    }
}

extension Coordinate: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(latitude, forKey: .latitude)
        try container.encode(longitude, forKey: .longitude)

        var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        try additionalInfo.encode(elevation, forKey: .elevation)
    }
}

But with MetaCodable all you have to write is this:

@Codable
struct Coordinate {
    var latitude: Double
    var longitude: Double

    @CodedAt("additionalInfo", "elevation")
    var elevation: Double
}

You can even minimize further using CodedIn macro since the final CodingKey value is the same as field name:

@Codable
struct Coordinate {
    var latitude: Double
    var longitude: Double

    @CodedIn("additionalInfo")
    var elevation: Double
}
Provide default value in case of decoding failures.

Instead of throwing error in case of missing data or type mismatch, you can provide a default value that will be assigned in this case. The following definition with MetaCodable:

@Codable
struct CodableData {
    @Default("some")
    let field: String
}

will not throw any error when empty JSON({}) or JSON with type mismatch({ "field": 5 }) is provided. The default value will be assigned in such case.

Also, memberwise initializer can be generated that uses this default value for the field.

@Codable
@MemberInit
struct CodableData {
    @Default("some")
    let field: String
}

The memberwise initializer generated will look like this:

init(field: String = "some") {
    self.field = field
}
Use or create custom helpers to provide custom decoding/encoding.

Library provides following helpers that address common custom decoding/encoding needs:

  • LossySequenceCoder to decode only valid data while ignoring invalid data in a sequence, instead of traditional way of failing decoding entirely.
  • ValueCoder to decode Bool, Int, Double, String etc. basic types even if they are represented in some other type, i.e decoding Int from "1", decoding boolean from "yes" etc.
  • Custom Date decoding/encoding with UNIX timestamp (Since1970DateCoder) or date formatters (DateCoder, ISO8601DateCoder).
  • Base64Coder to decode/encode data in base64 string representation.

And more, see the full documentation for HelperCoders for more details.

You can even create your own by conforming to HelperCoder.

Represent data with variations in the form of external/internal/adjacent tagging or lack of any tagging, with single enum with each case as a variation or a protocol type (lack of tagging not supported) that varies with conformances across modules.

i.e. while Swift compiler only generates implementation assuming external tagged enums, only following data:

[
  {
    "load": {
      "key": "MyKey"
    }
  },
  {
    "store": {
      "key": "MyKey",
      "value": 42
    }
  }
]

can be represented by following enum with current compiler implementation:

enum Command {
    case load(key: String)
    case store(key: String, value: Int)
}

while MetaCodable allows data in both of the following format to be represented by above enum as well:

[
  {
    "type": "load",
    "key": "MyKey"
  },
  {
    "type": "store",
    "key": "MyKey",
    "value": 42
  }
]
[
  {
    "type": "load",
    "content": {
      "key": "MyKey"
    }
  },
  {
    "type": "store",
    "content": {
      "key": "MyKey",
      "value": 42
    }
  }
]

See the full documentation for MetaCodable and HelperCoders, for API details and advanced use cases. Also, see the limitations.

Contributing

If you wish to contribute a change, suggest any improvements, please review our contribution guide, check for open issues, if it is already being worked upon or open a pull request.

License

MetaCodable is released under the MIT license. See LICENSE for details.