Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jmschonfeld/23393bcb6d5a2423d99d8b0174643fc9 to your computer and use it in GitHub Desktop.
Save jmschonfeld/23393bcb6d5a2423d99d8b0174643fc9 to your computer and use it in GitHub Desktop.

Top Level CodableWithConfiguration API for Foundation Coders

Revision history

  • v1 Initial version

Introduction / Motiviation

Currently, the TopLevelEncoder/TopLevelDecoder protocols along with Foundation's conformers (JSONEncoder, JSONDecoder, PropertyListEncoder, and PropertyListDecoder) offer an encode/decode function that accepts some Codable instance. In previous releases, we also introduced the CodableWithConfiguration protocol (which is now used by AttributedString and Predicate in Foundation) to allow for encoding/decoding types with extra provided information. Currently, CodableWithConfiguration types cannot be directly encoded/decoded by Foundation's coders. Instead, callers must wrap their CodableWithConfiguration type within a separate Codable type to provide to a coder like JSONEncoder. While in many cases this may already be necessary, in some cases this is unnecessary extra work developers must do in order to get started with serializing these types. With new adoption of CodableWithConfiguration for Predicate this year, we'd like to improve this experience.

Proposed solution and example

With these new APIs, developers will be able to directly encode/decode a CodableWithConfiguration type with Foundation's top level encoder/decoders. For example, developers will be able to write the following without needing to wrap the predicate in an arbitrary box type:

let predicate = #Predicate<Message> {
	$0.sender.firstName == "Jeremy"
}

let configuration: PredicateCodableConfiguration = /* ... */
let encoder = JSONEncoder()
let jsonData = encoder.encode(predicate, configuration: configuration)

For context, today the above code would need to be written with the following in order to invoke the existing APIs on the encoding/decoding containers:

let predicate = #Predicate<Message> {
	$0.sender.firstName == "Jeremy"
}


struct Box : Codable {
    let predicate: Predicate<Message>
    
    func encode(to encoder: Encoder) throws {
		let configuration: PredicateCodableConfiguration = /* ... */
    	var container = try encoder.unkeyedContainer()
    	try container.encode(predicate, configuration: configuration)
    }
    
    init(from decoder: Decoder) throws {
		let configuration: PredicateCodableConfiguration = /* ... */
    	var container = try decoder.unkeyedContainer()
    	predicate = try container.decode(Predicate<Message>.self, configuration: configuration)
    }
}

let encoder = JSONEncoder()
let jsonData = encoder.encode(Box(predicate: predicate))

Detailed design

We propose adding the following APIs to Foundation's various top level encoder/decoders:

@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
extension JSONEncoder {
	open func encode<T : EncodableWithConfiguration>(_ value: T, configuration: T.EncodingConfiguration) throws -> Data
	open func encode<T : EncodableWithConfiguration, C : EncodingConfigurationProviding>(_ value: T, configuration: C.Type) throws -> Data where T.EncodingConfiguration == C.EncodingConfiguration
}

@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
extension JSONDecoder {
	open func decode<T: DecodableWithConfiguration>(_ type: T.Type, from data: Data, configuration: T.DecodingConfiguration) throws -> T
	open func decode<T: DecodableWithConfiguration, C : DecodingConfigurationProviding>(_ type: T.Type, from data: Data, configuration: C.Type) throws -> T where T.DecodingConfiguration == C.DecodingConfiguration
}

@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
extension PropertyListEncoder {
	open func encode<Value : EncodableWithConfiguration>(_ value: Value, configuration: Value.EncodingConfiguration) throws -> Data
	open func encode<Value : EncodableWithConfiguration, C : EncodingConfigurationProviding>(_ value: Value, configuration: C.Type) throws -> Data where Value.EncodingConfiguration == C.EncodingConfiguration
}

extension PropertyListDecoder {
	open func decode<T : DecodableWithConfiguration>(_ type: T.Type, from data: Data, configuration: T.DecodingConfiguration) throws -> T
	open func decode<T : DecodableWithConfiguration, C : DecodingConfigurationProviding>(_ type: T.Type, from data: Data, configuration: C.Type) throws -> T where T.DecodingConfiguration == C.DecodingConfiguration
	
	open func decode<T : DecodableWithConfiguration>(_ type: T.Type, from data: Data, format: inout PropertyListSerialization.PropertyListFormat, configuration: T.DecodingConfiguration) throws -> T
	open func decode<T : DecodableWithConfiguration, C : DecodingConfigurationProviding>(_ type: T.Type, from data: Data, format: inout PropertyListSerialization.PropertyListFormat, configuration: C.Type) throws -> T where T.DecodingConfiguration == C.DecodingConfiguration
}

Impact on existing code

These changes are additive only; there is no impact on existing code.

Alternatives considered

Providing Additional API on TopLevelEncoder/TopLevelDecoder

Ideally, the generic protocols TopLevelEncoder & TopLevelDecoder would also have functions that accept CodableWithConfiguration types. However, given that these are already-shipping protocols we cannot add a new requirement without also providing a default implementation. Unfortunately, it is not possible to write a default implementation of these requirements. Instead, we've decided to add these new APIs directly to Foundation's encoders/decoders which will cover the majority of cases and leaves the flexibility for other third party coders to do the same if they are able.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment