Skip to content

Add conditional Sendable conformances#18

Merged
NicoMulet merged 1 commit into
mainfrom
nicolasm/sendable
May 12, 2026
Merged

Add conditional Sendable conformances#18
NicoMulet merged 1 commit into
mainfrom
nicolasm/sendable

Conversation

@NicoMulet
Copy link
Copy Markdown
Contributor

TL;DR

Add conditional Sendable conformances.

Context

With Swift 6's strict concurrency checking (and Xcode 26.5's default warnings), a value produced on the main actor and handed to a non-isolated @Sendable closure must conform to Sendable. None of the library's resource types declared Sendable, which forced every downstream app to add per-target retroactive conformances such as:

 extension JSONAPI.ResourceBody: @retroactive @unchecked Sendable {}

Conditional Sendable belongs in the library, mirroring how it already conditionally provides Equatable and Codable.

Summary of Changes

  • Add unconditional Sendable conformance to Unit, ResourceIdentifier, RelationshipOne, RelationshipMany — their stored properties are always Sendable.
  • Add conditional Sendable conformance to Resource, ResourceBody, InlineRelationshipOne, InlineRelationshipMany, InlineRelationshipOptional, CompoundDocument, DefaultEmpty — gated on
    the generic parameters that show up in stored properties.
  • @ResourceWrapper macro now propagates Sendable to the generated Attributes and Relationships structs when the wrapper itself conforms to Sendable (same detection pattern already used
    for Equatable).
  • Add a public init() to Unit so it's constructible from outside the module (used by the new tests).
  • Add SendableTests (11 type-witness checks) and a testResourceWrapperPropagatesSendable macro snapshot test to lock the behavior in.

How to Test

N/A — covered by the new tests.

Demo

  @ResourceWrapper(type: "people")
  struct Person: Sendable {
      var id: String

      @ResourceAttribute var firstName: String
      @ResourceRelationship var related: Person?
  }

  // `Person.Body` is now `Sendable`, so values can flow into a non-isolated
  // `@Sendable` closure without "sending main actor-isolated 'body' risks
  // causing data races" warnings.
  let body = Person.createBody(id: "1", firstName: "Dan")
  Task.detached {
      await send(body)
  }

@NicoMulet NicoMulet requested review from alanf and gonzalezreal May 12, 2026 09:27
Copy link
Copy Markdown

@alanf alanf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with a question about phantom copy

}
}

// `Destination` is phantom — `RelationshipMany` only stores `[ResourceIdentifier]` — so the
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Destination is phantom

I have no idea what this means...what does phantom mean here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Destination is not really stored inside RelationshipMany, it is only here to provide the ID type from the ResourceLinkageProviding. I think it fits the definition of Swift Phantom Types, but happy to change that if you think that's not relevant

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL!

@NicoMulet NicoMulet merged commit c6f790c into main May 12, 2026
1 check passed
@NicoMulet NicoMulet deleted the nicolasm/sendable branch May 12, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants