I recently released v1.0 of CodableWrappers (posted here) if you would like to discuss the library). It's features seem possibly worth writing my first Pitch, so I'd like to gauge the interest level in a more fleshed out pitch and if so, discuss how it would work in Swift (or probably the standard library).
Motivation
Currently the only method of adding custom Encoding/Decoding for Codable Types (without a custom implementation) is by adding options to the Encoder/Decoder. Although this is relatively fleshed out in JSON(En/De)Coder
, there are still some major pain points.
- Options must be set for every (En/De)coder used
- The option Types are separate so e.g. dateEncodingStrategy and dateDecodingStrategy have separate implementations and are both required to handle full serialization.
- The options aren't Portable and each (en/de)coder must supply their own options. Not even Swift's own PropertyList(En/De)coder has support for the same options as it's JSON cousin.
- It's all or nothing. e.g. If a Type or it's children need to use more than a single
dateEncodingStrategy
they must manage it themselves.
Proposed solution
Add a set of Property Wrappers that customize the Codable
implementation at the property level.
Examples
struct MyDataType: Codable {
@Base64Coding
var myData: Data // Now encodes to a Base64 String
}
struct MyDateType: Codable {
@SecondsSince1970DateCoding
var myDate: Date // Now encodes to SecondsSince1970
}
Customization that requires additional info can be passed with a Generic Type adopting a protocol with the necessary requirements.
struct MyNonConformingValueProvider: NonConformingDecimalValueProvider {
static var positiveInfinity: String = "100"
static var negativeInfinity: String = "-100"
static var nan: String = "-1"
}
struct MyFloatType: Codable {
@NonConformingFloatCoding<MyNonConformingValueProvider>
var myFloat: Float // Now encodes with the MyNonConformingValueProvider values for infinity/NaN
}
Detailed design
The relevant source for the library is here and a more technical writeup here. The current design uses a combination of protocols and generic @propertyWrappers to enable concise Property Wrappers and easy extension by adhering to a single protocol. This is then combined with a set of typealias
es to define the specific desired Wrappers
Here's a simplified version of the protocols/wrapper:
public protocol StaticCoder: Codable {
associatedtype CodingType: Encodable
/// Mirror of `Encodable`'s `encode(to: Encoder)` but in a static context
static func encode(value: CodingType, to encoder: Encoder) throws
/// Mirror of `Decodable`'s `init(from: Decoder)` but in a static context
static func decode(from decoder: Decoder) throws -> CodingType
}
public protocol StaticCodingWrapper: Codable {
associatedtype CustomCoder: StaticCoder
}
extension StaticCodingWrapper {
public init(from decoder: Decoder) throws {
self.init(wrappedValue: try CustomCoder.decode(from: decoder))
}
public func encode(to encoder: Encoder) throws {
try CustomCoder.encode(value: wrappedValue, to: encoder)
}
}
@propertyWrapper
public struct CodingUses<CustomCoder: StaticCoder>: StaticCodingWrapper {
public let wrappedValue: CustomCoder.CodingType
public init(wrappedValue: CustomCoder.CodingType) {
self.wrappedValue = wrappedValue
}
}
So for base64 Data
...
public struct Base64DataStaticCoder: StaticCoder {
public static func decode(from decoder: Decoder) throws -> Data {
let stringValue = try String(from: decoder)
guard let value = Data.init(base64Encoded: stringValue) else {
//throw an error
}
return value
}
public static func encode(value: Data, to encoder: Encoder) throws {
try value.base64EncodedString().encode(to: encoder)
}
}
// Full set of Property Wrappers
typealias Base64Encoding = EncodingUses<Base64DataStaticCoder>
typealias Base64Decoding = DecodingUses<Base64DataStaticCoder>
typealias Base64Coding = CodingUses<Base64DataStaticCoder>
typealias Base64EncodingMutable = EncodingUsesMutable<Base64DataStaticCoder>
typealias Base64DecodingMutable = DecodingUsesMutable<Base64DataStaticCoder>
typealias Base64CodingMutable = CodingUsesMutable<Base64DataStaticCoder>
This results in several advantages over what's currently available:
- It's Declarative. Serialization details can be constrained to the property definition rather than split between (en/de)coders and documentation
- Extendable. Adding a custom encoding option is as simple as implementing
StaticCoder
- Single declaration. The customization happens at the property level so it applies to all Encoders and Decoders. 1st party or otherwise
- Enables multiple (de/en)coding strategies within a single data structure without customizing the Codable implementation
Integration
Although this could replace the current options in JSON(En/De)coder
, these approaches can coexist without any issues. If desired the relevant APIs could be deprecated in favor of this approach or if the current API is still desired their implementation could also be re-routed to use the StaticCoder
s added by the new approach.
Possible Roadblocks
Property Wrapper Ergonomics
Since a Property Wrapper decides the mutability of it's wrapped Property, separate mutable versions of each property are required to support all use cases. e.g. Base64Coding
and Base64CodingMutable
. I can see this being improved at some point but I haven't seen any indication if/when that would be taking place.
Possible API Confusion
The naming around this is...tricky. I can see potential confusion between the new APIs and the current Codable
APIs. If this were to be added to the language some iteration on naming will likely be necessary.
Alternatives Considered
If it's preferable not to expose all the Protocols/Types currently included as public, each Property Wrapper could be a full implementation with the encoding/decoding done directly in the Wrapper (or just StaticCodingWrapper
removed). It would be more implementation heavy but could be done via code generation. This would remove the "Extendable" advantage, but would retain the others.
Future Directions
One ability I've been itching for is to customize a Property's CodingKey with Attribute syntax rather than having to override and maintain the entirety of CodingKeys
.
struct MyType: Codable {
@CodingKey("first-name")
var firstName: String
}
As far as I can tell, this is not currently possible with a Property Wrapper (if someone knows otherwise, please let me know how!). The simplest implementation I can see would be a step in CodingKeys generation that uses the passed name instead of the default.
Anyway, thanks for reading. Any feedback is appreciated!