[swift-evolution] [Proposal] Foundation Swift Encoders

Will Stanton willstanton1 at yahoo.com
Wed Mar 15 20:44:39 CDT 2017


Hello,

+1

This proposal seems helpful in standardizing how JSON objects can be written, and I commonly encode+decode JSON. The standard library JSON and PLIST encoders of Python are a strength, and Swift should be able to handle both formats just as easily. Still reading 'Swift Archival & Serialization’, but I believe both proposals will improve the safety and saneness of serializing/deserialization.

For the JSON coder, how does `deferredToDate` work? Would both the writer and reader have to agree to use `deferredToDate`?
Might it be better to force clients to pick a ‘real’ strategy? Why not default to one of the formats, perhaps ISO-8601?

(Not too important but also curious how much of a slowdown there will be when Xcode/SourceKit tries to autocomplete ‘enc’ or ‘dec’ for the Swift Archival & Serialization proposal?)

Regards,
Will Stanton

> On Mar 15, 2017, at 6:43 PM, Itai Ferber via swift-evolution <swift-evolution at swift.org> wrote:
> 
> Hi everyone,
> This is a companion proposal to the Foundation Swift Archival & Serialization API. This introduces new encoders and decoders to be used as part of this system.
> The proposal is available online and inlined below.
> 
> — Itai
> 
> Swift Encoders
> 	• Proposal: SE-NNNN
> 	• Author(s): Itai Ferber, Michael LeHew, Tony Parker
> 	• Review Manager: TBD
> 	• Status: Awaiting review
> 	• Associated PRs:
> 		• #8124
> Introduction
> As part of the proposal for a Swift archival and serialization API (SE-NNNN), we are also proposing new API for specific new encoders and decoders, as well as introducing support for new Codable types in NSKeyedArchiver and NSKeyedUnarchiver.
> 
> This proposal composes the latter two stages laid out in SE-NNNN.
> 
> Motivation
> With the base API discussed in SE-NNNN, we want to provide new encoders for consumers of this API, as well as provide a consistent story for bridging this new API with our existing NSCoding implementations. We would like to offer a base level of support that users can depend on, and set a pattern that third parties can follow in implementing and extending their own encoders.
> 
> Proposed solution
> We will:
> 
> 	• Add two new encoders and decoders to support encoding Swift value trees in JSON and property list formats
> 	• Add support for passing Codable Swift values to NSKeyedArchiver and NSKeyedUnarchiver, and add Codable conformance to our Swift value types
> Detailed design
> New Encoders and Decoders
> 
> JSON
> 
> One of the key motivations for the introduction of this API was to allow safer interaction between Swift values and their JSON representations. For values which are Codable, users can encode to and decode from JSON with JSONEncoder and JSONDecoder:
> 
> open class JSONEncoder {
> 
>     
> // MARK: Top-Level Encoding
> 
> 
>     
> /// Encodes the given top-level value and returns its JSON representation.
> 
>     
> ///
> 
>     
> /// - parameter value: The value to encode.
> 
>     
> /// - returns: A new `Data` value containing the encoded JSON data.
> 
>     
> /// - throws: `CocoaError.coderInvalidValue` if a non-comforming floating-point value is encountered during archiving, and the encoding strategy is `.throw`.
> 
>     
> /// - throws: An error if any value throws an error during encoding.
> 
>     open 
> func encode<Value : Codable>(_ value: Value) throws -> Data
> 
> 
>     
> // MARK: Customization
> 
> 
>     
> /// The formatting of the output JSON data.
> 
>     
> public enum OutputFormatting {
> 
>         
> /// Produce JSON compacted by removing whitespace. This is the default formatting.
> 
>         
> case
>  compact
> 
>         
> /// Produce human-readable JSON with indented output.
> 
>         
> case
>  prettyPrinted
>     
> }
> 
> 
>     
> /// The strategy to use for encoding `Date` values.
> 
>     
> public enum DateEncodingStrategy {
> 
>         
> /// Defer to `Date` for choosing an encoding. This is the default strategy.
> 
>         
> case
>  deferredToDate
> 
>         
> /// Encode the `Date` as a UNIX timestamp (as a JSON number).
> 
>         
> case
>  secondsSince1970
> 
>         
> /// Encode the `Date` as UNIX millisecond timestamp (as a JSON number).
> 
>         
> case
>  millisecondsSince1970
> 
>         
> /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
> 
>         @
> available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
> 
>         
> case
>  iso8601
> 
>         
> /// Encode the `Date` as a string formatted by the given formatter.
> 
>         
> case formatted(DateFormatter)
> 
> 
>         
> /// Encode the `Date` as a custom value encoded by the given closure.
> 
>         
> ///
> 
>         
> /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty `.default` container in its place.
> 
>         
> case custom((_ value: Date, _ encoder: Encoder) throws -> Void)
> 
>     
> }
> 
> 
>     
> /// The strategy to use for encoding `Data` values.
> 
>     
> public enum DataEncodingStrategy {
> 
>         
> /// Encoded the `Data` as a Base64-encoded string. This is the default strategy.
> 
>         
> case
>  base64
> 
>         
> /// Encode the `Data` as a custom value encoded by the given closure.
> 
>         
> ///
> 
>         
> /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty `.default` container in its place.
> 
>         
> case custom((_ value: Data, _ encoder: Encoder) throws -> Void)
> 
>     
> }
> 
> 
>     
> /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
> 
>     
> public enum NonConformingFloatEncodingStrategy {
> 
>         
> /// Throw upon encountering non-conforming values. This is the default strategy.
> 
>         
> case `throw
> `
> 
>         
> /// Encode the values using the given representation strings.
> 
>         
> case convertToString(positiveInfinity: String, negativeInfinity: String, nan: String)
> 
>     
> }
> 
> 
>     
> /// The output format to produce. Defaults to `.compact`.
> 
>     open 
> var outputFormatting: OutputFormatting
> 
> 
>     
> /// The strategy to use in encoding dates. Defaults to `.deferredToDate`.
> 
>     open 
> var dateEncodingStrategy: DateEncodingStrategy
> 
> 
>     
> /// The strategy to use in encoding binary data. Defaults to `.base64`.
> 
>     open 
> var dataEncodingStrategy: DataEncodingStrategy
> 
> 
>     
> /// The strategy to use in encoding non-conforming numbers. Defaults to `.throw`.
> 
>     open 
> var nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
> }
> 
> 
> open 
> class JSONDecoder {
> 
>     
> // MARK: Top-Level Decoding
> 
> 
>     
> /// Decodes a top-level value of the given type from the given JSON representation.
> 
>     
> ///
> 
>     
> /// - parameter type: The type of the value to decode.
> 
>     
> /// - parameter data: The data to decode from.
> 
>     
> /// - returns: A value of the requested type.
> 
>     
> /// - throws: `CocoaError.coderReadCorrupt` if values requested from the payload are corrupted, or if the given data is not valid JSON.
> 
>     
> /// - throws: An error if any value throws an error during decoding.
> 
>     open 
> func decode<Value : Codable>(_ type: Value.Type, from data: Data) throws -> Value
> 
> 
>     
> // MARK: Customization
> 
> 
>     
> /// The strategy to use for decoding `Date` values.
> 
>     
> public enum DateDecodingStrategy {
> 
>         
> /// Defer to `Date` for decoding. This is the default strategy.
> 
>         
> case
>  deferredToDate
> 
>         
> /// Decode the `Date` as a UNIX timestamp from a JSON number.
> 
>         
> case
>  secondsSince1970
> 
>         
> /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
> 
>         
> case
>  millisecondsSince1970
> 
>         
> /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
> 
>         @
> available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
> 
>         
> case
>  iso8601
> 
>         
> /// Decode the `Date` as a string parsed by the given formatter.
> 
>         
> case formatted(DateFormatter)
> 
> 
>         
> /// Decode the `Date` as a custom value decoded by the given closure.
> 
>         
> case custom((_ decoder: Decoder) throws -> Date)
> 
>     
> }
> 
> 
>     
> /// The strategy to use for decoding `Data` values.
> 
>     
> public enum DataDecodingStrategy {
> 
>         
> /// Decode the `Data` from a Base64-encoded string. This is the default strategy.
> 
>         
> case
>  base64
> 
>         
> /// Decode the `Data` as a custom value decoded by the given closure.
> 
>         
> case custom((_ decoder: Decoder) throws -> Data)
> 
>     
> }
> 
> 
>     
> /// The strategy to use for non-JSON-conforming floating-point values (IEEE 754 infinity and NaN).
> 
>     
> public enum NonConformingFloatDecodingStrategy {
> 
>         
> /// Throw upon encountering non-conforming values. This is the default strategy.
> 
>         
> case `throw
> `
> 
>         
> /// Decode the values from the given representation strings.
> 
>         
> case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
> 
>     
> }
> 
> 
>     
> /// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
> 
>     open 
> var dateDecodingStrategy: DateDecodingStrategy
> 
> 
>     
> /// The strategy to use in decoding binary data. Defaults to `.base64`.
> 
>     open 
> var dataDecodingStrategy: DataDecodingStrategy
> 
> 
>     
> /// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`.
> 
>     open 
> var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
> }
> Usage:
> 
> var encoder = JSONEncoder()
> 
> encoder
> .dateEncodingStrategy = .
> iso8601
> encoder
> .dataEncodingStrategy = .custom(myBase85Encoder)
> 
> 
> 
> // Since JSON does not natively allow for infinite or NaN values, we can customize strategies for encoding these non-conforming values.
> 
> encoder
> .nonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN")
> 
> 
> 
> // MyValue conforms to Codable
> let topLevel = MyValue(...)
> 
> 
> 
> let payload: Data
> do {
> 
>     payload 
> = try encoder.encode(topLevel)
> } catch {
> 
>     
> // Some value threw while encoding.
> }
> 
> 
> 
> // ...
> 
> 
> 
> var decoder = JSONDecoder()
> 
> decoder
> .dateDecodingStrategy = .
> iso8601
> decoder
> .dataDecodingStrategy = .custom(myBase85Decoder)
> 
> 
> 
> // Look for and match these values when decoding `Double`s or `Float`s.
> 
> decoder
> .nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN")
> 
> 
> 
> let topLevel: MyValue
> do {
> 
>     topLevel 
> = try decoder.decode(MyValue.self, from: payload)
> } catch {
> 
>     
> // Data was corrupted, or some value threw while decoding.
> }
> It should be noted here that JSONEncoder and JSONDecoder do not themselves conform to Encoder and Decoder; instead, they contain private nested types which do conform to Encoder and Decoder, which are passed to values' encode(to:)and init(from:). This is because JSONEncoder and JSONDecoder must present a different top-level API than they would at intermediate levels.
> 
> Property List
> 
> We also intend to support the property list format, with PropertyListEncoder and PropertyListDecoder:
> 
> open class PropertyListEncoder {
> 
>     
> // MARK: Top-Level Encoding
> 
> 
>     
> /// Encodes the given top-level value and returns its property list representation.
> 
>     
> ///
> 
>     
> /// - parameter value: The value to encode.
> 
>     
> /// - returns: A new `Data` value containing the encoded property list data.
> 
>     
> /// - throws: An error if any value throws an error during encoding.
> 
>     open 
> func encode<Value : Codable>(_ value: Value) throws -> Data
> 
> 
>     
> // MARK: Customization
> 
> 
>     
> /// The output format to write the property list data in. Defaults to `.binary`.
> 
>     open 
> var outputFormat: PropertyListSerialization.PropertyListFormat
> }
> 
> 
> open 
> class PropertyListDecoder {
> 
>     
> // MARK: Top-Level Decoding
> 
> 
>     
> /// Decodes a top-level value of the given type from the given property list representation.
> 
>     
> ///
> 
>     
> /// - parameter type: The type of the value to decode.
> 
>     
> /// - parameter data: The data to decode from.
> 
>     
> /// - returns: A value of the requested type.
> 
>     
> /// - throws: `CocoaError.coderReadCorrupt` if values requested from the payload are corrupted, or if the given data is not a valid property list.
> 
>     
> /// - throws: An error if any value throws an error during decoding.
> 
>     open 
> func decode<Value : Codable>(_ type: Value.Type, from data: Data) throws -> Value
> 
> 
>     
> /// Decodes a top-level value of the given type from the given property list representation.
> 
>     
> ///
> 
>     
> /// - parameter type: The type of the value to decode.
> 
>     
> /// - parameter data: The data to decode from.
> 
>     
> /// - parameter format: The parsed property list format.
> 
>     
> /// - returns: A value of the requested type along with the detected format of the property list.
> 
>     
> /// - throws: `CocoaError.coderReadCorrupt` if values requested from the payload are corrupted, or if the given data is not a valid property list.
> 
>     
> /// - throws: An error if any value throws an error during decoding.
> 
>     open 
> func decode<Value : Codable>(_ type: Value.Type, from data: Data, format: inout PropertyListSerialization.PropertyListFormat) throws -> Value
> }
> Usage:
> 
> let encoder = PropertyListEncoder()
> let topLevel = MyValue(...)
> let payload: Data
> do {
> 
>     payload 
> = try encoder.encode(topLevel)
> } catch {
> 
>     
> // Some value threw while encoding.
> }
> 
> 
> 
> // ...
> 
> 
> 
> let decoder = PropertyListDecoder()
> let topLevel: MyValue
> do {
> 
>     topLevel 
> = try decoder.decode(MyValue.self, from: payload)
> } catch {
> 
>     
> // Data was corrupted, or some value threw while decoding.
> }
> Like with JSON, PropertyListEncoder and PropertyListDecoder also provide private nested types which conform to Encoder and Decoder for performing the archival.
> 
> Foundation-Provided Errors
> 
> Along with providing the above encoders and decoders, we would like to promote the use of a common set of error codes and messages across all new encoders and decoders. A common vocabulary of expected errors allows end-users to write code agnostic about the specific encoder/decoder implementation they are working with, whether first-party or third-party:
> 
> extension CocoaError.Code {
> 
>     
> /// Thrown when a value incompatible with the output format is encoded.
> 
>     
> public static var coderInvalidValue: CocoaError.Code
> 
> 
>     
> /// Thrown when a value of a given type is requested but the encountered value is of an incompatible type.
> 
>     
> public static var coderTypeMismatch: CocoaError.Code
> 
> 
>     
> /// Thrown when read data is corrupted or otherwise invalid for the format. This value already exists today.
> 
>     
> public static var coderReadCorrupt: CocoaError.Code
> 
> 
>     
> /// Thrown when a requested key or value is unexpectedly null or missing. This value already exists today.
> 
>     
> public static var coderValueNotFound: CocoaError.Code
> }
> 
> 
> 
> // These reexpose the values above.
> extension CocoaError {
> 
>     
> public static var coderInvalidValue: CocoaError.Code
> 
> 
>     
> public static var coderTypeMismatch: CocoaError.Code
> }
> The localized description strings associated with the two new error codes are:
> 
> 	• .coderInvalidValue: "The data is not valid for encoding in this format."
> 	• .coderTypeMismatch: "The data couldn't be read because it isn't in the correct format." (Precedent from NSCoderReadCorruptError.)
> All of these errors will include the coding key path that led to the failure in the error's userInfo dictionary under NSCodingKeyContextErrorKey, along with a non-localized, developer-facing failure reason under NSDebugDescriptionErrorKey.
> 
> NSKeyedArchiver & NSKeyedUnarchiver Changes
> 
> Although our primary objectives for this new API revolve around Swift, we would like to make it easy for current consumers to make the transition to Codable where appropriate. As part of this, we would like to bridge compatibility between new Codabletypes (or newly-Codable-adopting types) and existing NSCoding types.
> 
> To do this, we want to introduce changes to NSKeyedArchiver and NSKeyedUnarchiver in Swift that allow archival of Codable types intermixed with NSCoding types:
> 
> // These are provided in the Swift overlay, and included in swift-corelibs-foundation.
> extension NSKeyedArchiver {
> 
>     
> public func encodeCodable(_ codable: Codable?, forKey key: String) { ... }
> }
> 
> 
> 
> extension NSKeyedUnarchiver {
> 
>     
> public func decodeCodable<T : Codable>(_ type: T.Type, forKey key: String) -> T? { ... }
> }
> NOTE: Since these changes are being made in extensions in the Swift overlay, it is not yet possible for these methods to be overridden. These can therefore not be added to NSCoder, since NSKeyedArchiver and NSKeyedUnarchiver would not be able to provide concrete implementations. In order to call these methods, it is necessary to downcast from an NSCoder to NSKeyedArchiver/NSKeyedUnarchiver directly. Since subclasses of NSKeyedArchiver and NSKeyedUnarchiver in Swift will inherit these implementations without being able to override them (which is wrong), we will NSRequiresConcreteImplementation() dynamically in subclasses.
> The addition of these methods allows the introduction of Codable types into existing NSCoding structures, allowing for a transition to Codable types where appropriate.
> 
> Refining encode(_:forKey:)
> 
> Along with these extensions, we would like to refine the import of -[NSCoder encodeObject:forKey:], which is currently imported into Swift as encode(_: Any?, forKey: String). This method currently accepts Objective-C and Swift objects conforming to NSCoding (non-conforming objects produce a runtime error), as well as bridgeable Swift types (Data, String, Array, etc.); we would like to extend it to support new Swift Codable types, which would otherwise produce a runtime error upon call.
> 
> -[NSCoder encodeObject:forKey:] will be given a new Swift name of encodeObject(_:forKey:), and we will provide a replacement encode(_: Any?, forKey: String) in the overlay which will funnel out to either encodeCodable(_:forKey:) or encodeObject(_:forKey:) as appropriate. This should maintain source compatibility for end users already calling encode(_:forKey:), as well as behavior compatibility for subclassers of NSCoderand NSKeyedArchiver who may be providing their own encode(_:forKey:).
> 
> Semantics of Codable Types in Archives
> 
> There are a few things to note about including Codable values in NSKeyedArchiverarchives:
> 
> 	• Bridgeable Foundation types will always bridge before encoding. This is to facilitate writing Foundation types in a compatible format from both Objective-C and Swift
> 		• On decode, these types will decode either as their Objective-C or Swift version, depending on user need (decodeObject(forKey:) will decode as an Objective-C object; decodeCodable(_:forKey:) as a Swift value)
> 	• User types, which are not bridgeable, do not write out a $class and can only be decoded in Swift. In the future, we may add API to allow Swift types to provide an Objective-C class to decode as, effectively allowing for user bridging across archival
> Foundation Types Adopting Codable
> 
> The following Foundation Swift types will be adopting Codable, and will encode as their bridged types when encoded through NSKeyedArchiver, as mentioned above:
> 
> 	• AffineTransform
> 	• Calendar
> 	• CharacterSet
> 	• Date
> 	• DateComponents
> 	• DateInterval
> 	• Decimal
> 	• IndexPath
> 	• IndexSet
> 	• Locale
> 	• Measurement
> 	• Notification
> 	• PersonNameComponents
> 	• TimeZone
> 	• URL
> 	• URLComponents
> 	• URLRequest
> 	• UUID
> Along with these, the Array, Dictionary, and Set types will gain Codableconformance (as part of the Conditional Conformance feature), and encode through NSKeyedArchiver as NSArray, NSDictionary, and NSSet respectively.
> 
> Source compatibility
> The majority of this proposal is additive. The changes to NSKeyedArchiver are intended to be non-source-breaking changes, and non-behavior-breaking changes for subclasses in Objective-C and Swift.
> 
> Effect on ABI stability
> The addition of this API will not be an ABI-breaking change. However, this will add limitations for changes in future versions of Swift, as parts of the API will have to remain unchanged between versions of Swift (barring some additions, discussed below).
> 
> Effect on API resilience
> Much like new API added to the standard library, once added, some changes to this API will be ABI- and source-breaking changes. Changes to the new encoder and decoder classes provided above will be restricted as described in the library evolution document in the Swift repository; in particular, the removal of methods or nested types or changes to argument types will break client behavior. Additionally, additions to provided options enums will be a source-breaking change for users performing an exhaustive switch over their cases; removal of cases will be ABI-breaking.
> 
> Alternatives considered
> None. This is a companion to the Swift Archival and Serialization API.
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution



More information about the swift-evolution mailing list