0028-REVISE-ATTACHMENT-ENCODABLE-INTERFACES: Revise Swift Testing's `Attachment`/`Encodable` interop
- Proposal: ST-0028
- Authors: Jonathan Grynspan
- Review Manager: Paul LeMarquand
- Status: Status: Active Review (Aug 10...Aug 24, 2026)
- Implementation: swiftlang/swift-testing#1770
- Review: (pitch)
Introduction
We introduced attachments to Swift Testing in Swift 6.2 with [ST-0009]. As part of that initial introduction, we included default implementations of our [Attachable] protocol's requirements for types that already conform to [Encodable] or [NSSecureCoding].
This proposal revises the interfaces for attaching a value that conforms to one of those protocols to resolve some constraints that come with the current interface and implementation.
Motivation
We have identified several deficiencies in the current interface:
- Test authors must explicitly add conformance to [
Attachable] to their
types that already conform to [Encodable] or [NSSecureCoding], which is boilerplate that doesn't really do anything other than satisfy Swift's type system.
- The only way to select between property list and JSON encodings is to specify
a path extension in the attachment's preferred name.
- There is no way to customize the encoder's settings. For example, there is no
way to configure JSONEncoder.outputFormatting.
- Types that conform to both [
Encodable] and [NSSecureCoding] are
ambiguously encoded. We document these types as using the [Encodable] encoding by default, but there is no way to customize this behavior.
Proposed solution
New [Attachment] initializers are introduced that behave similarly to the initializers introduced for file URLs in [ST-0009] and for [Transferable]-conforming types in [ST-0023].
The existing default implementations of [Attachable]'s requirements are marked to-be-deprecated. Their documentation will automatically include migration information, but new diagnostics will not be emitted at build time until a future Swift release.
Detailed design
The following extension to Attachment is added and is available when test authors import both Testing and Foundation in a Swift file:
extension Attachment {
/// Initialize an instance of this type representing a value that conforms to
/// the `Encodable` protocol.
///
/// - Parameters:
/// - encodableValue: The value to encode and attach.
/// - encodingFormat: The encoding format to use to encode `encodableValue`.
/// - preferredName: The preferred name of the attachment when writing it to
/// a test report or to disk.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
///
/// - Throws: If an appropriate encoder could not be found given the
/// `encodingFormat` and `preferredName` arguments.
///
/// Use this initializer to create an instance of ``Attachment`` from a value
/// that conforms to the `Encodable` protocol:
///
/// let menu = FoodTruck.currentMenu
/// let attachment = try Attachment(encoding: menu, as: .json)
/// Attachment.record(attachment)
///
/// The encoding that the testing library uses depends on the `encodingFormat`
/// argument. If the value of that argument is `nil`, the testing library
/// derives the format from the path extension you specify in `preferredName`.
///
/// | Extension | Encoding Used | Encoder Used |
/// |-|-|-|
/// | `".xml"` | XML property list | `PropertyListEncoder` |
/// | `".plist"` | Binary property list | `PropertyListEncoder` |
/// | None, `".json"` | JSON | `JSONEncoder` |
///
/// - Important: OpenStep-style property lists are not supported.
///
/// If the values of both the `encodingFormat` and `preferredName` arguments
/// are `nil`, the testing library encodes `encodableValue` as JSON.
public init<T>(
encoding encodableValue: T,
as encodingFormat: AttachableEncodingFormat? = nil,
named preferredName: String? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) throws where AttachableValue == _AttachableEncodableWrapper<T, Void>, T: Encodable
#if canImport(Combine)
/// Initialize an instance of this type representing a value that conforms to
/// the `Encodable` protocol.
///
/// - Parameters:
/// - encodableValue: The value to encode and attach.
/// - encoder: The encoder to use to encode `encodableValue`.
/// - preferredName: The preferred name of the attachment when writing it to
/// a test report or to disk.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
///
/// - Throws: If `encoder` cannot be used to encode `encodableValue`.
///
/// Use this initializer to create an instance of ``Attachment`` from a value
/// that conforms to the `Encodable` protocol:
///
/// let menu = FoodTruck.currentMenu
/// let encoder = JSONEncoder()
/// let attachment = try Attachment(encoding: menu, using: encoder)
/// Attachment.record(attachment)
public init<T, E>(
encoding encodableValue: T,
using encoder: E,
named preferredName: String? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) throws where AttachableValue == _AttachableEncodableWrapper<T, E>, T: Encodable, E: TopLevelEncoder & Sendable, E.Output: ContiguousBytes
#endif
/// Initialize an instance of this type representing a value that conforms to
/// the `NSSecureCoding` protocol.
///
/// - Parameters:
/// - encodableValue: The value to encode and attach.
/// - propertyListFormat: The property list format to use to encode
/// `encodableValue`.
/// - preferredName: The preferred name of the attachment when writing it to
/// a test report or to disk.
/// - sourceLocation: The source location of the call to this initializer.
/// This value is used when recording issues associated with the
/// attachment.
///
/// - Throws: If an appropriate encoder could not be found given the
/// `propertyListFormat` and `preferredName` arguments.
///
/// Use this initializer to create an instance of ``Attachment`` from a value
/// that conforms to the `NSSecureCoding` protocol:
///
/// let menu = FoodTruck.currentMenu
/// let attachment = try Attachment(encoding: menu, as: .xml)
/// Attachment.record(attachment)
///
/// The encoding that the testing library uses depends on the
/// `propertyListFormat` argument. If the value of that argument is `nil`, the
/// testing library derives the format from the path extension you specify in
/// `preferredName`.
///
/// | Extension | Encoding Used | Encoder Used |
/// |-|-|-|
/// | `".xml"` | XML property list | `NSKeyedArchiver` |
/// | None, `".plist"` | Binary property list | `NSKeyedArchiver` |
///
/// - Important: OpenStep-style property lists are not supported.
///
/// If the values of both the `propertyListFormat` and `preferredName`
/// arguments are `nil`, the testing library encodes `encodableValue` as a
/// binary property list.
public init<T>(
encoding encodableValue: T,
as propertyListFormat: PropertyListSerialization.PropertyListFormat? = nil,
named preferredName: String? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) throws where AttachableValue == _AttachableEncodableWrapper<T, NSKeyedArchiver>, T: NSSecureCoding
}[!NOTE] The
_AttachableEncodableWrapperis a type that conforms to the [AttachableWrapper] protocol and is an implementation detail. Test authors do not need to use this type directly.
The AttachableEncodingFormat type is declared as follows:
/// An enumeration describing the encoding formats that you can use when
/// attaching a value that conforms to `Encodable`.
///
/// Pass an instance of this type to ``Testing/Attachment/init(encoding:as:named:sourceLocation:)``
/// to specify what encoder and format to use when the testing library saves the
/// resulting attachment.
///
/// If you want to attach a value that conforms to `NSSecureCoding`, use
/// `PropertyListFormat` instead.
public struct AttachableEncodingFormat: Sendable, Equatable {
/// Create an instance of this type representing a property list format.
///
/// - Parameters:
/// - format: The corresponding property list format.
///
/// - Returns: An instance of this type representing `format`.
public static func propertyListFormat(_ format: PropertyListSerialization.PropertyListFormat) -> Self
/// An instance of this type representing the JSON format.
public static var json: Self { get }
}Test authors can use the new initializers to create attachments from any value that conforms to [Encodable] or [NSSecureCoding]. These values do not need a pro forma conformance to [Attachable]:
@Test func `Bake an apple pie`() throws {
let oven = Oven()
oven.warm(to: .fahrenheit(400))
...
let attachment = try Attachment(encoding: recipe, as: .propertyListFormat(.binary))
Attachment.record(attachment)
...
#expect(pie.isDelicious)
}
// OR:
@Test func `Bake an apple pie`() throws {
let oven = Oven()
oven.warm(to: .fahrenheit(400))
...
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, ...]
let attachment = try Attachment(encoding: recipe, using: encoder)
Attachment.record(attachment)
...
#expect(pie.isScrumptious)
}If the test author does not specify an encoding format or encoder, Swift Testing makes a best effort to derive the encoding format from the attachment's preferred name (as with the existing interface). If the test author doesn't specify a preferred name either, Swift Testing defaults to JSON (for [Encodable] types) or the binary property list format (for [NSSecureCoding] types).
Deprecation of existing interfaces
The existing default implementations are marked to-be-deprecated:
extension Attachable where Self: Encodable {
/// @DeprecationSummary {
/// Use ``Attachment/init(encoding:as:named:sourceLocation:)`` instead:
///
/// let attachment = try Attachment(encoding: someValue, as: .json)
/// Attachment.record(attachment)
/// }
@available(swift, introduced: 6.2, deprecated: 100000.0, message: "Use 'Attachment.init(encoding:as:named:sourceLocation:)' instead")
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
}
extension Attachable where Self: NSSecureCoding {
/// @DeprecationSummary {
/// Use ``Attachment/init(encoding:as:named:sourceLocation:)`` instead:
///
/// let attachment = try Attachment(encoding: someValue, as: .binary)
/// Attachment.record(attachment)
/// }
@available(swift, introduced: 6.2, deprecated: 100000.0, message: "Use 'Attachment.init(encoding:as:named:sourceLocation:)' instead")
public func withUnsafeBytes<R>(for attachment: borrowing Attachment<Self>, _ body: (UnsafeRawBufferPointer) throws -> R) throws -> R
}TopLevelEncoder on non-Apple platforms
[TopLevelEncoder] is declared in the Combine framework which is part of Apple's SDKs but not part of the open source Swift project. As such, it is not available on non-Apple platforms. See future directions for more details; in the mean time, concrete overloads of init(encoding:using:named:sourceLocation:) are added on non-Apple platforms to cover the common use cases:
#if !canImport(Combine)
extension Attachment {
public init<T, E>(
encoding encodableValue: T,
using encoder: E,
named preferredName: String? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) throws where AttachableValue == _AttachableEncodableWrapper<T, E>, T: Encodable, E: PropertyListEncoder & Sendable
public init<T, E>(
encoding encodableValue: T,
using encoder: E,
named preferredName: String? = nil,
sourceLocation: SourceLocation = #_sourceLocation
) throws where AttachableValue == _AttachableEncodableWrapper<T, E>, T: Encodable, E: JSONEncoder & Sendable
}
#endifSource compatibility
These changes are additive and should not impact existing code.
Integration with supporting tools
No changes. Supporting tools that consume attachments should "just work".
Future directions
- Lowering [
TopLevelEncoder] to the standard library. If we do this, then
the protocol becomes available on non-Apple platforms and our hard-coded workarounds can be removed. (The author intends to propose a change here, but it is beyond the scope of this proposal).
- Formally deprecating the existing interface. As discussed above, a future
Swift toolchain release will emit deprecation warnings at build time when a type conforms to both [Encodable]/[NSSecureCoding] and [Attachable] and relies on the default implementations of [Attachable]'s requirements.
Alternatives considered
- Exposing new overloads of
Attachment.record()instead ofAttachment.init().
See [ST-0023] for more information why we prefer to overload init() here instead of record().
- Dropping support for [
NSSecureCoding]. While [Encodable] is the
preferred serialization protocol in Swift, [NSSecureCoding] remains supported at the Foundation layer.
- Leaving the existing interfaces undeprecated while adding the new ones.
The existing interfaces have the aforementioned ergonomic deficiencies. We don't want test authors to add conformances to [Attachable] by rote, and we don't want to support two entirely distinct mechanisms for attaching encodable values when only one is needed (let alone where only one is recommended).
[Attachable]: https://developer.apple.com/documentation/testing/attachable [AttachableWrapper]: https://developer.apple.com/documentation/testing/attachablewrapper [Attachment]: https://developer.apple.com/documentation/testing/attachment [Encodable]: https://developer.apple.com/documentation/swift/encodable [NSSecureCoding]: https://developer.apple.com/documentation/foundation/nssecurecoding [TopLevelEncoder]: https://developer.apple.com/documentation/combine/toplevelencoder [Transferable]: https://developer.apple.com/documentation/coretransferable/transferable
[ST-0009]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0009-attachments.md [ST-0023]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0023-attachments-transferable.md