GitHub - SwiftyLab/MetaCodable: Supercharge Swift's Codable implementations with macros meta-programming. (original) (raw)
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 withCodedAt(_:)
passing single argument, instead of requiring you to write all theCodingKey
values. - Allows to create flattened model for nested
CodingKey
values withCodedAt(_:)
andCodedIn(_:)
. - Allows to create composition of multiple
Codable
types withCodedAt(_:)
passing no arguments. - Allows to read data from additional fallback
CodingKey
s provided withCodedAs(_:_:)
. - Allows to provide default value in case of decoding failures with
Default(_:)
, or only in case of failures when missing value withDefault(ifMissing:)
. Different default values can also be used for value missing and other errors respectively withDefault(ifMissing:forErrors:)
. - Allows to create custom decoding/encoding strategies with
HelperCoder
and using them withCodedBy(_:)
,CodedBy(_:properties:)
or others. i.e.LossySequenceCoder
etc. - Allows applying common strategies like
ValueCoder
to all properties of a type through theCodable(commonStrategies:)
parameter, reducing the need for repetitive property annotations. - Allows specifying different case values with
CodedAs(_:_:)
and case value/protocol type identifier type different fromString
withCodedAs()
. - Allows specifying enum-case/protocol type identifier path with
CodedAt(_:)
and case content path withContentAt(_:_:)
. - 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()
andIgnoreEncoding()
. Allows to ignore encoding based on custom conditions withIgnoreEncoding(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
HelperCoder
s withMetaProtocolCodable
build tool plugin fromDynamicCodable
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 target
s of your choosing, by adding it to the dependencies
value of your target
s.
.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
:
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 decodeBool
,Int
,Double
,String
etc. basic types even if they are represented in some other type, i.e decodingInt
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.