Skip to content

Ability to attach macros to generated types and methods #383

Open

Description

@HonQii

It would be nice to be able to add macros to the generated Schema and API requests in order to extend the functionality, e.g. all of my request return structures have an optional error field, so if I could add a macro that would allow me to extend the support for a protocol for them, I could easily handle the error uniformly

Activity

  1. simonjbeaumont commented on Nov 17, 2023

    @simonjbeaumont
    Collaborator

    @HonQii could you elaborate a little on what you'd like to see? You can currently write extensions on the types generated and conform them to protocols that you define. Are you asking for the protocol conformance to be automatically added for you by the generator, potentially by specifying a macro you'd like to run in the generator config?

    Maybe you could provide a quick snippet of how you'd like the generated code to be different.

  2. czechboy0 commented on Nov 17, 2023

    @czechboy0
    Contributor

    Maybe @HonQii means that they could configure some macro name to be automatically attached to all generated types, similar to how we allow adding additional imports? But the user would still need to provide the macro implementation.

    (Which is an interesting idea that would indeed provide quite a lot of flexibility for sufficiently motivated adopters.)

  3. HonQii commented on Nov 20, 2023

    @HonQii
    Author

    Maybe @HonQii means that they could configure some macro name to be automatically attached to all generated types, similar to how we allow adding additional imports? But the user would still need to provide the macro implementation.🔄  ❓

    (Which is an interesting idea that would indeed provide quite a lot of flexibility for sufficiently motivated adopters.)🔄  ❓

    Right, what I mean is that I can configure macros to the generated Schema as well as the Client so that I can do some wrapping. An example:

    // Each of my request response structures will have an error field with a message and code if there is an error
    struct BaseResponse {
          var error: Error? // empty if there is no error.
    }

    Normally I would need to define a protocol for all structures to follow, and then I could handle these errors consistently. But in the case of autogeneration, I think it's more appropriate to use a macro for this purpose

    Similarly, I need a layer of encapsulation for each request in the client to handle exceptions, which can also be accomplished using a macro

  4. czechboy0 commented on Nov 24, 2023

    @czechboy0
    Contributor

    Can you be even more specific here, for example use a simple OpenAPI document, generate code with it, and then outline what exact changes you'd like to see in the generated code? I'm still not entirely sure I understand.

  5. simonjbeaumont commented on Nov 27, 2023

    @simonjbeaumont
    Collaborator
    // Each of my request response structures will have an error field with a message and code if there is an error
    struct BaseResponse {
          var error: Error? // empty if there is no error.
    }

    OOI, are you the author of this document? Because, if so, for this example, you could have the generated code make use of shared types by writing this in the OpenAPI document itself. For example, you can make use of of a common type in #/components/schemas, or you could define a shared response in #/components/responses, or you could try and combine these with an allOf1.

    Concretely, if you make use of references in the OpenAPI document, then the generator will use a single type. This allows for you to write extensions on this common type once.

    Footnotes

    1. https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#composition-and-inheritance-polymorphism ↩

  6. HonQii commented on Nov 28, 2023

    @HonQii
    Author

    Can you be even more specific here, for example use a simple OpenAPI document, generate code with it, and then outline what exact changes you'd like to see in the generated code? I'm still not entirely sure I understand.您能否在这里更具体一点,例如使用一个简单的 OpenAPI 文档,用它生成代码,然后概述您希望在生成的代码中看到哪些确切的更改?我仍然不完全确定我是否理解。

    For example, I'm using the Combine framework to encapsulate a lot of UI components, so I'd like to be able to return web requests through the Publisher, and I'm now generating my own batch of code using the Plugin, which is formatted as follows:

    func getMyInfo(_ query: Components.Schemas.GetMyInfoReq) -> AnyPublisher<Components.Schemas.GetMyInfoResp, Error> {
            Future { promise in
                Task {
                    do {
                        let resp = try await self.filterSuccess(await self.client.getMyInfo(.init(body: .json(query))))
                        promise(.success(resp))
                    } catch {
                        promise(.failure(error))
                    }
                }
            }
            .eraseToAnyPublisher()
        }

    If I could support macros when generating swift-openapi-generator, I could generate the methods directly from the macros, and instead of having to convert the results using the filterSuccess method, I could just make each structure in the Schema adhere to a certain protocol, and use Combine's filter function to handle the errors.

  7. HonQii commented on Nov 28, 2023

    @HonQii
    Author
    // Each of my request response structures will have an error field with a message and code if there is an error
    struct BaseResponse {
          var error: Error? // empty if there is no error.
    }

    OOI, are you the author of this document? Because, if so, for this example, you could have the generated code make use of shared types by writing this in the OpenAPI document itself. For example, you can make use of of a common type in #/components/schemas, or you could define a shared response in #/components/responses, or you could try and combine these with an allOf1.OOI,你是本文档的作者吗?因为,如果是这样,对于此示例,您可以通过将其编写在 OpenAPI 文档本身中来让生成的代码使用共享类型。例如,您可以在 中使用 中的通用类型,也可以在 中定义共享响应,或者可以尝试将这些响应 #/components/schemas 与 allOf 1 . #/components/responses

    Concretely, if you make use of references in the OpenAPI document, then the generator will use a single type. This allows for you to write extensions on this common type once.具体来说,如果使用 OpenAPI 文档中的引用,则生成器将使用单个类型。这允许您在此常见类型上编写一次扩展。

    1. https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#composition-and-inheritance-polymorphism ↩

    Sorry, I didn't write it.

  8. czechboy0 commented on Nov 28, 2023

    @czechboy0
    Contributor

    If I understand correctly, you'd be interested in being able to use a macro to generate a variant of every client method that uses Combine, like you outlined above?

    If so, that seems very similar to the "add async variant" macro here: https://github.com/apple/swift-syntax/blob/main/Examples/Sources/MacroExamples/Implementation/Peer/AddAsyncMacro.swift

    The question is where exactly the generator would add the @UserSpecifiedMacro macro, probably in the extension, so that the macro could emit an implementation (couldn't be on the func declaration in the protocol). I think that could work, and can be done post-1.0 as a non-breaking change, as this would be fully opt-in through the config file, similar to additionalImports.

  9. changed the title [-]Can I add some parameters to specify that the generated Schema and API support swift macros?[/-] [+]Ability to attach macros to generated types and methods[/+] on Nov 29, 2023
  10. czechboy0 commented on Nov 29, 2023

    @czechboy0
    Contributor

    Okay, renaming this issue to track the general desire to attach macros to generated types and functions.

  11. added
    status/needs-designNeeds further discussion and a concrete proposal.
    and removed
    status/triageCollecting information required to triage the issue.
    on Nov 29, 2023
  12. added this to the Post-1.0 milestone on Nov 29, 2023
  13. farzadshbfn commented on Jun 18, 2026

    @farzadshbfn

    @czechboy0 I just want to provide more use-cases that could benefit a lot from a Macro and are not possible via extensions.

    I currently have a @Mockable macro which I apply to protocols, to generate a mock container for testing.

    For example the following code:

    @Mockable
    protocol SomeAPIProtocol {
        func login(username: String, password: String) async throws(AuthError) -> Result
    }

    Would automatically generate:

    #if DEBUG
    final class MockSomeAPIProtocol: SomeAPIProtocol, Sendable, Mocking.Withable {
        @MockedMethod
        func login(username: String, password: String) async throws(AuthError) -> Result
    }
    #endif
    
    extension SomeAPIProtocol where Self == MockSomeAPIProtocol {
        #if DEBUG
        static var $mock: MockMacroTests.MockSomeAPIProtocol {
            MockMacroTests.MockSomeAPIProtocol()
        }
        #endif
    }
    

    Right now, this is the only part of my code-base that I have to manually maintain and introduce the stub MockSomeAPIProtocol for every API and provide each method with mocked macro manually, yet the same part of my code-base, which is using auto generated code that breaks a lot.

    I also can't introduce a variable with $ in an extension, as it is only allowed through macros and propertyWrappers.

    Being able to ship it with the library itself, would be a great help.

  14. mackoj commented on Oct 1, 2026

    @mackoj

    Hello, FWIW I have worked on something in this scope too.

    Since generated code gets regenerated on every build, so there's no way to attach attributes or attached macros to generated declarations. The generator rewrites the files on every build, so the only options are post-processing them or forking the generator.

    What I propose is pretty straightforward. Adding a new optional config key, called macroAnnotations(name could change) for now, lists attributes per generated declaration:

    additionalImports:
      - MyMacros
    macroAnnotations:
      schemas:            # Components.Schemas.*
        "*": ["@MyMacro"]
        Pet: ["@PetMacro"]
      operations:         # Operations.<operationId>.Input / .Output
        "*":
          output: ["@ResponseMacro"]
        createPet:
          input: ["@InputMacro"]
      apiProtocol: ["@MyProtocolMacro"]
      client: ["@ClientMacro"]

    * matches every schema or operation. The generator emits its attributes first, then the ones for the specific name. Each attribute goes on its own line after the doc comment:

    /// - Remark: Generated from `#/components/schemas/Pet`.
    @MyMacro
    @PetMacro
    public struct Pet: Codable, Hashable, Sendable { ... }

    It should work for both clients and server so everything except client targets Types.swift, which client and server share. Server handlers conform to APIProtocol, which apiProtocol covers. Server.swift only extends APIProtocol and UniversalServer, so there's nothing server-specific to annotate.

  15. mackoj commented on Oct 1, 2026

    @mackoj

    Some Examples to illustrate that it could work for all the cases showed in this thread.

    Shared handling of responses

    This is the use case from this issue. If every response has an optional error field, a macro that adds a protocol conformance can go on every operation output:

    macroAnnotations:
      operations:
        "*":
          output: ["@APIErrorReporting"]

    swift-dependencies' @DependencyClient

    This one was not in the thread but it's one the reason that I have worked on it. @DependencyClient from swift-dependencies needs a struct of closures. The generated Client has methods instead, so the macro can't go on it. Today you write a wrapper by hand and update it whenever the document changes:

    @DependencyClient
    struct PetStoreClient {
        var listPets: @Sendable (Operations.ListPets.Input) async throws -> Operations.ListPets.Output
        var createPet: @Sendable (Operations.CreatePet.Input) async throws -> Operations.CreatePet.Output
        // ... one per operation
    }
    
    extension PetStoreClient: DependencyKey {
        static var liveValue: Self {
            let client = Client(serverURL: ..., transport: URLSessionTransport())
            return Self(
                listPets: { try await client.listPets($0) },
                createPet: { try await client.createPet($0) }
            )
        }
    }

    With apiProtocol, a peer macro on APIProtocol can generate this wrapper. When you add an operation to the document, the next build adds its closure to the wrapper.

    additionalImports:
      - PetStoreMacros
    macroAnnotations:
      apiProtocol: ["@DependencyClientWrapper(name: \"PetStoreClient\")"]
    @DependencyClientWrapper(name: "PetStoreClient")
    public protocol APIProtocol: Sendable { ... }
    
    // Macro expansion:
    @DependencyClient
    public struct PetStoreClient {
        public var listPets: @Sendable (Operations.ListPets.Input) async throws -> Operations.ListPets.Output
        public var createPet: @Sendable (Operations.CreatePet.Input) async throws -> Operations.CreatePet.Output
        public static func live(_ api: some APIProtocol) -> Self { ... }
    }

    The adopter or a library provides @DependencyClientWrapper. The generator only attaches it. live(_:) gives the liveValue, and @DependencyClient gives unimplemented closures for tests, so nobody maintains the wrapper by hand. That's what I was after in the forum thread.

    Test mocks

    Mockable generates a mock from a protocol. On APIProtocol, the mock works for testing both client callers and server handlers:

    additionalImports:
      - Mockable
    macroAnnotations:
      apiProtocol: ["@Mockable"]
  16. czechboy0 commented on Oct 2, 2026

    @czechboy0
    Contributor

    Thanks for fleshing this out, @mackoj, I really like where this is going.

    A few things to dig into:

    1. Is there really no "server" macro we imagine needing, for example to attach to the registerHandlers generated method? Do we see a need there?
    2. Can we flesh out the exact way the schema name matching works? You showed "*" and an exact match, is that all? Or should it be a regex, meaning an exact match would need to be "^Pet$". Is it case sensitive matching?
    3. Can we follow the structure of the doc and make it operations/schemas rather than just schemas at the top level?
    4. Do we support other components, like parameters, requestBodies, etc?

    If you'd prefer, I'm open to you opening a draft PR with a proposal, and we can iterate on some of the details there. But the general shape you have here looks good.

  17. mackoj commented on Oct 2, 2026

    @mackoj

    Thanks for the feedback @czechboy0,

    1. I have to admit that I didn't really think about how to push the server part before for this feature. Let's keep a server key for symmetry with client. It would target the generated registerHandlers(on:serverURL:configuration:middlewares:) method. For example, it could be used for a tracing or logging macro that wraps handler registration. It's a small addition, and it means the server side isn't a blind spot if someone needs it later. Did you have more in minds?

    2. True, it's vague and underdefined because in my head I only needed those. I think going full Regex is more coherent than having a pseudo "*" next to exact names like "Pet". I think that the rules could be:

      • each key is a regex matched against the OpenAPI name (e.g. pet-store), not the Swift-safe name
      • to match Pet you will need to write ^Pet$ to not match PetOwner too
      • matching is case-sensitive and will follow the Regex
      • .* replaces "*" to match everything
      • when several keys match, their attributes are all emitted but the exact ordering still needs to be pinned down
    3. Yes, for sure

    4. Would something like this work?

    macroAnnotations:
      components:
        schemas:
          ".*": ["@MyMacro"]
          Pet: ["@PetMacro"]
        parameters:
          ".*": ["@ParameterMacro"]
        requestBodies:
          ".*": ["@RequestBodyMacro"]
        responses:
          ".*": ["@ResponseMacro"]
        headers:
          ".*": ["@HeaderMacro"]
      operations:
        createPet:
          input: ["@InputMacro"]
      apiProtocol: [...]
      client: [...]
      server: [...]

    This covers every component the generator turns into a type (Components.Schemas, Components.Parameters, Components.RequestBodies, Components.Responses, and Components.Headers). The other component kinds (examples, links, callbacks, security schemes) don't produce generated types today, so there would be nothing to annotate

    Thanks for offering the proposal and the PR. I'll open a draft PR with a SOAR proposal so we can iterate on the details there.

  18. czechboy0 commented on Oct 2, 2026

    @czechboy0
    Contributor

    Yes that's looking much better. One more consideration - how do we express macros that take parameters?

  19. mackoj commented on Oct 2, 2026

    @mackoj

    I was thinking something like this '@DependencyClientWrapper(name: "PetStoreClient")' . I need a bit more time to think about how to express dynamic parameters.(bad idea)

  20. czechboy0 commented on Oct 3, 2026

    @czechboy0
    Contributor

    Let's also drop the @ sign. The generator will always add it to macro names. But yeah, how parameters will be expressed in the config is something to figure out. Maybe an array of dictionaries with explicit label and value fields?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status/needs-designNeeds further discussion and a concrete proposal.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions