Endpoints 4.0.0 brings full Swift 6.2+ support with strict concurrency compliance, requiring Sendable conformance throughout the library and introducing the Session actor for thread-safe networking.
-
Swift 6.2+ Strict Concurrency
- All core protocols now require
Sendableconformance Sessionis now an actor, ensuring thread-safe access to URLSession- All
Call,Client, andResponseParsertypes must beSendable ResponseParser.OutputTypemust beSendablefor safe concurrent access
- All core protocols now require
-
AnyClient Renamed to DefaultClient
- The
AnyClientclass has been renamed toDefaultClientand changed from a class to a struct DefaultClientis now a value type (struct) conforming toSendable- The
open classpattern for subclassing is no longer supported
- The
-
Client Protocol Changes
- Client protocol now requires
Sendableconformance - Removed default protocol extensions that previously allowed delegation to an internal
clientproperty - Custom clients must now implement all methods directly (typically by delegating to a
DefaultClientinstance) - Changed from
func encode<C: Call>(call: C)tofunc encode(call: some Call)for improved ergonomics
- Client protocol now requires
-
Parameter Naming Updates
DefaultClientinitializer:baseURLparameter renamed tourl- Example:
DefaultClient(url: myURL)instead ofDefaultClient(baseURL: myURL)
-
JSONParser Improvements
- Default
JSONParsernow includes standard configuration:dateDecodingStrategy = .iso8601keyDecodingStrategy = .convertFromSnakeCase
- Custom decoder configuration can still be achieved by creating a custom parser
- Default
-
Session Changes
Sessionis now an actor for thread-safe networking- All
Sessionmethods must be called withawaitfrom outside the actor context debugparameter added to Session initializer for request/response logging
- Updated minimum Swift version requirement to 6.2+
- Updated CI workflows to use Xcode 26.0 and macOS 15
- Applied SwiftFormat across the codebase
- Moved example project to separate repository (Endpoints-Example)
Before (3.x):
let client = AnyClient(baseURL: URL(string: "https://api.example.com")!)After (4.x):
let client = DefaultClient(url: URL(string: "https://api.example.com")!)In 3.x, you could subclass AnyClient using the open class pattern. In 4.x, you must use composition with a struct.
Before (3.x):
class MyAPIClient: AnyClient {
var apiKey = "secret"
override func encode<C: Call>(call: C) async throws -> URLRequest {
var request = try await super.encode(call: call)
request.addValue(apiKey, forHTTPHeaderField: "API-Key")
return request
}
}After (4.x):
struct MyAPIClient: Client {
private let client: Client
let apiKey = "secret"
init() {
let url = URL(string: "https://api.example.com")!
self.client = DefaultClient(url: url)
}
func encode(call: some Call) async throws -> URLRequest {
var request = try await client.encode(call: call)
request.addValue(apiKey, forHTTPHeaderField: "API-Key")
return request
}
func parse<C>(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType
where C: Call {
try await client.parse(response: response, data: data, for: call)
}
func validate(response: HTTPURLResponse?, data: Data?) async throws {
try await client.validate(response: response, data: data)
}
}Before (3.x):
let session = Session(with: client)
let (body, response) = try await session.dataTask(for: call)After (4.x):
// Session is now an actor - same usage pattern but with actor isolation
let session = Session(with: client)
let (body, response) = try await session.dataTask(for: call)
// If you need debug logging:
let session = Session(with: client, debug: true)The syntax remains the same, but Session is now actor-isolated. This means:
- All access is automatically serialized and thread-safe
- You must use
awaitwhen calling session methods from outside the actor - You cannot directly access session properties without
await
All custom Call, Client, and response types must now be Sendable.
Before (3.x):
struct GetUser: Call {
typealias Parser = JSONParser<User>
let userId: String
var request: URLRequestEncodable {
Request(.get, "users/\(userId)")
}
}
struct User: Codable {
let id: String
let name: String
}After (4.x):
// Call types must be Sendable (structs with Sendable properties are automatically Sendable)
struct GetUser: Call {
typealias Parser = JSONParser<User>
let userId: String // String is Sendable
var request: URLRequestEncodable {
Request(.get, "users/\(userId)")
}
}
// Response types must be Sendable
struct User: Codable, Sendable { // Add explicit Sendable conformance
let id: String
let name: String
}Important: If your custom types contain non-Sendable properties (like closures, class instances, or other reference types), you'll need to refactor them to use value types or actor-isolated references.
Before (3.x):
struct CustomParser<T: Decodable>: ResponseParser {
typealias OutputType = T
func parse(data: Data, encoding: String.Encoding) throws -> T {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(T.self, from: data)
}
}After (4.x):
struct CustomParser<T: Decodable & Sendable>: ResponseParser { // T must be Sendable
typealias OutputType = T // OutputType is automatically Sendable
func parse(data: Data, encoding: String.Encoding) throws -> T {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return try decoder.decode(T.self, from: data)
}
}The ValidationBlock closure type is now marked as @Sendable.
Before (3.x):
public typealias ValidationBlock = (HTTPURLResponse?, Data?) throws -> VoidAfter (4.x):
public typealias ValidationBlock = @Sendable (HTTPURLResponse?, Data?) throws -> VoidThis means any closures passed to AnyCall for validation must be @Sendable, which requires them to only capture Sendable values.
Solution: Ensure your client is a struct (value type) and all its stored properties are Sendable. If you have reference types, consider using actors or refactoring to value types.
Solution: Make sure any stored clients are themselves Sendable. DefaultClient is Sendable, so storing it works. If you're storing a custom client, ensure it also conforms to Sendable.
Solution: Add Sendable conformance to your model types:
struct MyModel: Codable, Sendable {
// ...
}Solution: Since Session is now an actor, all calls to its methods require await:
let (body, response) = try await session.dataTask(for: call)- Thread Safety: The actor-based
Sessionensures safe concurrent access to networking - Data Race Prevention:
Sendableconformance prevents data races at compile time - Swift 6 Future-Proofing: Full compatibility with Swift's modern concurrency model
- Better Composition: Struct-based clients encourage better composition patterns over inheritance
- Type Safety: Improved type inference with
some Callparameter
For more examples, see the Endpoints-Example repository.