Skip to content

Latest commit

 

History

History
250 lines (190 loc) · 8.17 KB

File metadata and controls

250 lines (190 loc) · 8.17 KB

v4.0.0 - Changelog & Migration Guide

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.

Changelog

Developer Facing

  • Swift 6.2+ Strict Concurrency

    • All core protocols now require Sendable conformance
    • Session is now an actor, ensuring thread-safe access to URLSession
    • All Call, Client, and ResponseParser types must be Sendable
    • ResponseParser.OutputType must be Sendable for safe concurrent access
  • AnyClient Renamed to DefaultClient

    • The AnyClient class has been renamed to DefaultClient and changed from a class to a struct
    • DefaultClient is now a value type (struct) conforming to Sendable
    • The open class pattern for subclassing is no longer supported
  • Client Protocol Changes

    • Client protocol now requires Sendable conformance
    • Removed default protocol extensions that previously allowed delegation to an internal client property
    • Custom clients must now implement all methods directly (typically by delegating to a DefaultClient instance)
    • Changed from func encode<C: Call>(call: C) to func encode(call: some Call) for improved ergonomics
  • Parameter Naming Updates

    • DefaultClient initializer: baseURL parameter renamed to url
    • Example: DefaultClient(url: myURL) instead of DefaultClient(baseURL: myURL)
  • JSONParser Improvements

    • Default JSONParser now includes standard configuration:
      • dateDecodingStrategy = .iso8601
      • keyDecodingStrategy = .convertFromSnakeCase
    • Custom decoder configuration can still be achieved by creating a custom parser
  • Session Changes

    • Session is now an actor for thread-safe networking
    • All Session methods must be called with await from outside the actor context
    • debug parameter added to Session initializer for request/response logging

Internal

  • 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)

Migration Guide

AnyClient → DefaultClient

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")!)

Custom Client Implementation

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)
    }
}

Session is Now an Actor

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 await when calling session methods from outside the actor
  • You cannot directly access session properties without await

Sendable Conformance for Custom Types

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.

Custom ResponseParser

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)
    }
}

AnyCall ValidationBlock

The ValidationBlock closure type is now marked as @Sendable.

Before (3.x):

public typealias ValidationBlock = (HTTPURLResponse?, Data?) throws -> Void

After (4.x):

public typealias ValidationBlock = @Sendable (HTTPURLResponse?, Data?) throws -> Void

This means any closures passed to AnyCall for validation must be @Sendable, which requires them to only capture Sendable values.

Common Migration Issues

Issue: "Type 'MyClient' does not conform to protocol 'Sendable'"

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.

Issue: "Stored property 'client' of 'Sendable'-conforming struct has non-sendable type"

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.

Issue: "Cannot pass argument of non-sendable type 'MyModel' to parameter of type 'some Sendable'"

Solution: Add Sendable conformance to your model types:

struct MyModel: Codable, Sendable {
    // ...
}

Issue: "Expression is 'async' but is not marked with 'await'"

Solution: Since Session is now an actor, all calls to its methods require await:

let (body, response) = try await session.dataTask(for: call)

Benefits of 4.x

  • Thread Safety: The actor-based Session ensures safe concurrent access to networking
  • Data Race Prevention: Sendable conformance 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 Call parameter

For more examples, see the Endpoints-Example repository.