AutoCodable
exposes Swift macros that generate code to fulfill Encodable
and Decodable
requirements when adding the protocol conformance to an extension of a type in a different file.
The Swift's built-in Codable
API has a major advantage - it automatically synthesizes many different encoding and decoding implementations when using it. This behavior allows to easy create custom types and makes them conform to Encodable
or Decodable
without a need to implement func encode(to encoder: Encoder) throws
or init(from decoder: Decoder) throws
explicitly.
However, one of its limitations is the necessity to keep everything within the same file. It means that the following scenario may happen:
// User.swift
struct User {
let firstName: String
let lastName: String
}
// User+Decodable.swift
extension User: Decodable {
// π Extension outside of file declaring struct 'User' prevents
// automatic synthesis of 'init(from:)' for protocol 'Decodable'
}
// User+Encodable.swift
extension User: Encodable {
// π Extension outside of file declaring struct 'User' prevents
// automatic synthesis of 'encode(to:)' for protocol 'Encodable'
}
There may be many reasons why you would like to keep the conformance to Codable
or Decodable
outside of the file with the type declaration. Unfortunately, in such cases, it's required to implement it explicitly. The AutoDecodable
and AutoEncodable
macro fills this gap. It allows to generation of necessary code and still keeps the declaration separate from the conformance to the protocols.
@AutoEncodable
// User.swift
struct User {
let firstName: String
let lastName: String
}
// User+Encodable.swift
@AutoEncodable
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
}
π½
// User+Encodable.swift
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
}
}
@AutoEncodable + public access control
// User.swift
public struct User {
public let firstName: String
public let lastName: String
}
// User+Encodable.swift
@AutoEncodable(accessControl: .public)
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
}
π½
// User+Encodable.swift
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
}
}
@AutoEncodable + singleValueContainer
// Identifier.swift
struct Identifier {
let value: Int
}
// Identifier+Encodable.swift
//βοΈThe name associated with `singleValue` must match the property name inside the type.
@AutoEncodable(container: .singleValue("value"))
extension Identifier: Encodable {}
π½
// Identifier+Encodable.swift
extension Identifier: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
@AutoEncodable + nestedContainer
// User.swift
struct User {
let firstName: String
let lastName: String
}
// User+Encodable.swift
@AutoEncodable
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case names
//βοΈThe nested coding keys enum must follow the name convention: `CaseName` + `CodingKeys`
enum NamesCodingKeys: String, CodingKey {
case firstName
case lastName
}
}
}
π½
// User+Encodable.swift
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case names
enum NamesCodingKeys: String, CodingKey {
case firstName
case lastName
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var namesContainer = container.nestedContainer(
keyedBy: CodingKeys.NamesCodingKeys.self,
forKey: .names
)
try namesContainer.encode(firstName, forKey: .firstName)
try namesContainer.encode(lastName, forKey: .lastName)
}
}
@AutoEncodable + enum
// Membership.swift
enum Membership {
case regular
case premium
}
// Membership+Encodable.swift
@AutoEncodable(container: .singleValueForEnum)
extension Membership: Encodable {
enum CodingKeys: String, CodingKey {
case regular
case premium
}
}
π½
// Membership+Encodable.swift
extension Membership: Encodable {
enum CodingKeys: String, CodingKey {
case regular
case premium
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .regular:
try container.encode(CodingKeys.regular.rawValue)
case .premium:
try container.encode(CodingKeys.premium.rawValue)
}
}
}
@AutoEncodable + property custom encoding
// User.swift
struct User {
let firstName: String
let lastName: String
let avatarUrl: URL
}
// User+Encodable.swift
@AutoEncodable
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
@EncodedValue(Avatar.self)
case avatarUrl
}
private struct Avatar: EncodableValue {
let path: String
let `extension`: String
init(from value: URL) {
self.path = value.deletingPathExtension().absoluteString
self.extension = value.pathExtension
}
}
}
π½
// User+Encodable.swift
extension User: Encodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
@EncodedValue(Avatar.self)
case avatarUrl
}
private struct Avatar: EncodableValue {
let path: String
let `extension`: String
init(from value: URL) {
self.path = value.deletingPathExtension().absoluteString
self.extension = value.pathExtension
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
try container.encode(Avatar(from: avatarUrl), forKey: .avatarUrl)
}
}
@AutoDecodable
// User.swift
struct User {
let firstName: String
let lastName: String
}
// User+Decodable.swift
@AutoDecodable
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
}
π½
// User+Decodable.swift
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
firstName: container.decode(for: .firstName),
lastName: container.decode(for: .lastName)
)
}
}
@AutoDecodable + public access control
// User.swift
public struct User {
public let firstName: String
public let lastName: String
}
// User+Decodable.swift
@AutoDecodable(accessControl: .public)
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
}
π½
// User+Decodable.swift
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
firstName: container.decode(for: .firstName),
lastName: container.decode(for: .lastName)
)
}
}
@AutoDecodable + singleValueContainer
// Identifier.swift
struct Identifier {
let value: Int
}
// Identifier+Decodable.swift
//βοΈThe name associated with `singleValue` must match the property name inside the type.
@AutoDecodable(container: .singleValue("value"))
extension Identifier: Encodable {}
π½
// Identifier+Decodable.swift
extension Identifier: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
try self.init(value: container.decode())
}
}
@AutoDecodable + nestedContainer
// User.swift
struct User {
let firstName: String
let lastName: String
}
// User+Decodable.swift
@AutoDecodable
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case names
//βοΈThe nested coding keys enum must follow the name convention: `CaseName` + `CodingKeys`
enum NamesCodingKeys: String, CodingKey {
case firstName
case lastName
}
}
}
π½
// User+Decodable.swift
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case names
enum NamesCodingKeys: String, CodingKey {
case firstName
case lastName
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let namesContainer = try container.nestedContainer(
keyedBy: CodingKeys.NamesCodingKeys.self,
forKey: .names
)
try self.init(
firstName: namesContainer.decode(for: .firstName),
lastName: namesContainer.decode(for: .lastName)
)
}
}
@AutoDecodable + enum
// Membership.swift
enum Membership {
case regular
case premium
}
// Membership+Decodable.swift
@AutoDecodable(container: .singleValueForEnum)
extension Membership: Decodable {
enum CodingKeys: String, CodingKey {
case regular
case premium
}
}
π½
// Membership+Decodable.swift
extension Membership: Decodable {
enum CodingKeys: String, CodingKey {
case regular
case premium
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let stringValue = try container.decode(String.self)
switch stringValue {
case CodingKeys.regular.rawValue:
self = .regular
case CodingKeys.premium.rawValue:
self = .premium
default:
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Invalid value: \(stringValue)"
)
}
}
}
@AutoDecodable + property custom decoding
// User.swift
struct User {
let firstName: String
let lastName: String
let avatarUrl: URL?
}
// User+Decodable.swift
@AutoDecodable
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
@DecodedValue(Avatar.self)
case avatarUrl
}
private struct Avatar: DecodableValue {
let path: String
let `extension`: String
func value() -> URL? {
.init(string: path + "." + `extension`)
}
}
}
π½
// User+Decodable.swift
extension User: Decodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
@DecodedValue(Avatar.self)
case avatarUrl
}
private struct Avatar: DecodableValue {
let path: String
let `extension`: String
func value() -> URL? {
.init(string: path + "." + `extension`)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
firstName: container.decode(for: .firstName),
lastName: container.decode(for: .lastName),
avatarUrl: container.decode(Avatar.self, forKey: .avatarUrl).value()
)
}
}
AutoCodable
is released under the MIT license. See the LICENSE file for more info.