Contents

swiftylab/metacodable

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(commonStrategies:)` 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(:), CodedBy(:properties:) or others. i.e. LossySequenceCoder` etc.
  • Allows applying common strategies like ValueCoder to all properties of a type through the `Codable(commonStrategies:)` parameter, reducing the need for repetitive property annotations.
  • 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

<details> <summary><h3>Swift Package Manager</h3></summary>

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"),

</details> <details> <summary><h3>CocoaPods</h3></summary>

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'

</details>

Usage

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

<details>
  <summary>Custom `CodingKey` value declaration per variable, instead of requiring you to write for all fields.</summary>

 i.e. in the official [docs](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types#2904057), to define custom `CodingKey` for 2 fields of `Landmark` type you had to write:

```swift
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:

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

    var location: Coordinate
    var vantagePoints: [Coordinate]
}
```

</details>

<details>
  <summary>Create flattened model for nested `CodingKey` values.</summary>

i.e. in official [docs](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types#2904058) to decode a JSON like this:

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

You had to write all these boilerplate:

```swift
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:

```swift
@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:

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

    @CodedIn("additionalInfo")
    var elevation: Double
}
```

</details>

<details>
  <summary>Provide default value in case of decoding failures.</summary>

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

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

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

The memberwise initializer generated will look like this:

```swift
init(field: String = "some") {
    self.field = field
}
```

</details>

<details>
  <summary>Use or create custom helpers to provide custom decoding/encoding.</summary>

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`](https://swiftpackageindex.com/SwiftyLab/MetaCodable/documentation/helpercoders) for more details.

You can even create your own by conforming to `HelperCoder`.

</details>

<details>
  <summary>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.</summary>

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

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

can be represented by following `enum` with current compiler implementation:

```swift
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:

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

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

</details>

See the full documentation for [`MetaCodable`](https://swiftpackageindex.com/SwiftyLab/MetaCodable/documentation/metacodable) and [`HelperCoders`](https://swiftpackageindex.com/SwiftyLab/MetaCodable/documentation/helpercoders), for API details and advanced use cases.
Also, [see the limitations](Sources/MetaCodable/MetaCodable.docc/Limitations.md).

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.

Package Metadata

Repository: swiftylab/metacodable

Default branch: main

README: README.md