# Customization and namespacing for swift-openapi-generator generated code

**URL:** <https://forums.swift.org/t/customization-and-namespacing-for-swift-openapi-generator-generated-code/81856>\
**Category:** Server\
**Tags:** openapi, dependency-injection\
**Created:** [August 28, 2025, 3:13pm UTC](https://forums.swift.org/t/customization-and-namespacing-for-swift-openapi-generator-generated-code/81856 "2025-08-28T15:13:00Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![mackoj](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mackoj/32/42395_2.png) [@mackoj](https://forums.swift.org/u/mackoj)\
**Post date:** [August 28, 2025, 3:13pm UTC](https://forums.swift.org/t/customization-and-namespacing-for-swift-openapi-generator-generated-code/81856/1 "2025-08-28T15:13:00Z")

</div>

Hello,

Thanks to the maintainers(Honza, Si) and contributors of `swift-openapi-generator`.

I have a couple of questions around customizing the generated code so it plays nicely when we generate multiple implementations in the same projects and extend it’s behaviour.

## What I’m trying to do

- I want to consume the generated client via `swift-dependencies` so that:

- Multiple generated client in one app(yes it’s not really server side)

If there’s already a supported way to do any of the above and I missed it, I’d love to be pointed in the right direction!

## Questions about customization (namespacing, symbols, files)

1. **Per-vendor namespacing:** Is there a supported way to disambiguate names when generating multiple clients into the same target?

2. **Renaming default symbols:** Can we configure defaults like `APIProtocol`, `Client`, and other types emitted in `Types.swift` and `Client.swift`? If not today, would a templated name option (e.g. `{{Vendor}}Client`, `{{Vendor}}APIProtocol`) be acceptable?

3. **File names/paths:** Is there a way to change the output filenames (e.g. `Github.Types.swift`, `Github.Client.swift`) or the output directory structure per spec? This would also help avoid collisions and improve discoverability.

4. **Codegen hooks / plugin point:** Is there a supported hook to:

A light-weight hook (template variable, naming option, or attribute injection) would solve both the collision problem and the `swift-dependencies` integration described below.

So… to make it work with `swift-dependencies` I did do a macro that turns the generated `Client` into a façade struct of function closures that mirrors the client’s methods. This façade is then trivial to inject via `Dependencies`.

> **If you are interested the macro it’s here it’s not great but it can give you an idea of what I’m trying to do.**
>
> When attached to the generated `struct Client`, `@OpenAPIFacade` synthesizes a **façade struct** whose properties are closures one per client instance method with the same signatures (parameters, `async`/`throws`, and return types). It also generates `.live(using:)` and `.live(baseURL:session:)` factories that adapt the real `Client` to the façade. The façade’s “all-closure” shape makes it ideal for integration with `swift-dependencies`: you register a façade value in the dependency graph, and in tests/previews you replace any endpoint by supplying a custom closure.
> 
> **Why it’s useful:**
> 
> - Works seamlessly with `Dependencies` (live, mock, preview)
> - No hand-written boilerplate per endpoint methods are mirrored automatically
> - Keeps signatures in sync with codegen over time
> 
> **Usage**
> 
> ```swift
> @OpenAPIFacade
> struct Client: APIProtocol { /* generated by swift-openapi-generator */ }
> 
> ```
> 
> **Code**
> 
> ```swift
> import SwiftCompilerPlugin
> import SwiftSyntax
> import SwiftSyntaxBuilder
> import SwiftSyntaxMacros
> import Foundation
> 
> @main
> struct Plugin: CompilerPlugin {
> let providingMacros: [Macro.Type] = [OpenAPIFacadeMacro.self]
> }
> 
> public struct OpenAPIFacadeMacro: PeerMacro {
> public static func expansion(
> of node: AttributeSyntax,
> providingPeersOf decl: some DeclSyntaxProtocol,
> in context: some MacroExpansionContext
> ) throws -> [DeclSyntax] {
> 
> guard let structDecl = decl.as(StructDeclSyntax.self) else {
> return [DeclSyntax(stringLiteral: """
> #error("`@OpenAPIFacade` must be attached to the `struct` declaration (e.g. the generated `Client`).")
> """)]
> }
> 
> let args = node.arguments?.as(LabeledExprListSyntax.self)
> let facadeName = stringArg(named: "facade", in: args) ?? "APIClient"
> let baseURL = stringArg(named: "baseURL", in: args) ?? "http://localhost:8080"
> 
> let clientType = structDecl.name.text
> 
> let methods: [FunctionDeclSyntax] = structDecl.memberBlock.members
> .compactMap { $0.decl.as(FunctionDeclSyntax.self) }
> .filter { fn in
> guard fn.name.text != "init" else { return false }
> let isStatic = fn.modifiers.contains(
> where: { $0.name.tokenKind == .keyword(.static)
> })
> if isStatic { return false }
> let isPrivate = fn.modifiers.contains(
> where: { ["private","fileprivate"].contains($0.name.text)
> })
> return !isPrivate
> }
> 
> var propDecls: [String] = []
> var initParams: [String] = []
> var initBody: [String] = []
> var liveUsingArms: [String] = []
> 
> for (idx, fn) in methods.enumerated() {
> let fnName = fn.name.text
> 
> let closureParamSig = makeClosureParamSignature(from: fn.signature.parameterClause.parameters, indexSeed: idx)
> 
> let callArgList = makeCallArgumentList(from: fn.signature.parameterClause.parameters, indexSeed: idx)
> 
> let asyncTok = fn.signature.effectSpecifiers?.asyncSpecifier?.text ?? ""
> let throwsTok = fn.signature.effectSpecifiers?.throwsClause?.throwsSpecifier.text ?? ""
> 
> let returnTy = fn.signature.returnClause?.type.description ?? "Void"
> 
> propDecls.append("public var \(fnName): @Sendable (\(closureParamSig)) \(asyncTok) \(throwsTok) -> \(returnTy)")
> 
> initParams.append("\(fnName): @escaping @Sendable (\(closureParamSig)) \(asyncTok) \(throwsTok) -> \(returnTy)")
> initBody.append("self.\(fnName) = \(fnName)")
> 
> liveUsingArms.append("""
> \(fnName): { \(closureParamSig) in
> return try \(asyncTok.isEmpty ? "" : "await ")client.\(fnName)(\(callArgList))
> }
> """)
> }
> 
> var pieces: [String] = []
> 
> pieces.append("""
> import Foundation
> import OpenAPIURLSession
> import OpenAPIRuntime
> import Dependencies
> """)
> 
> pieces.append("""
> public struct \(facadeName) {
> \(propDecls.map { " " + $0 }.joined(separator: "\n"))
>     
> public init(
> \(initParams.joined(separator: ",\n "))
> ) {
> \(initBody.map { " " + $0 }.joined(separator: "\n"))
> }
> 
> public static func live(using client: \(clientType)) -> Self {
> return Self(
> \(liveUsingArms.joined(separator: ",\n "))
> )
> }
> 
> public static func live(baseURL: URL, session: URLSession = .shared) -> Self {
> let transport = URLSessionTransport(configuration: .init(session: session))
> let client = \(clientType)(
> serverURL: baseURL,
> configuration: .init(),
> transport: transport
> )
> return .live(using: client)
> }
> 
> public static func live(session: URLSession = .shared) -> Self {
> .live(baseURL: URL(string: "\(baseURL)")!, session: session)
> }
> }
> """)
> 
> return [DeclSyntax(stringLiteral: pieces.joined(separator: "\n\n"))]
> }
> }
> 
> fileprivate func stringArg(named: String, in args: LabeledExprListSyntax?) -> String? {
> guard let expr = args?.first(where: { $0.label?.text == named })?.expression else { return nil }
> if let str = expr.as(StringLiteralExprSyntax.self)?.segments.first?.description {
> return str.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
> }
> if let seg = expr.as(StringLiteralExprSyntax.self)?.segments.first?.as(StringSegmentSyntax.self) {
> return seg.content.text
> }
> return expr.description
> }
> 
> fileprivate func boolArg(named: String, in args: LabeledExprListSyntax?) -> Bool? {
> guard let expr = args?.first(where: { $0.label?.text == named })?.expression else { return nil }
> return expr.description.trimmingCharacters(in: .whitespacesAndNewlines) == "true"
> }
> 
> fileprivate func makeClosureParamSignature(from params: FunctionParameterListSyntax, indexSeed: Int) -> String {
> var parts: [String] = []
> var counter = indexSeed
> for p in params {
> let name = (p.secondName ?? p.firstName)?.text ?? "arg\(counter)"; counter += 1
> let type = p.type.description
> let inoutPrefix = (p.type.as(AttributedTypeSyntax.self)?.specifier?.text == "inout") ? "inout " : ""
> parts.append("_ \(name): \(inoutPrefix)\(type)")
> }
> return parts.joined(separator: ", ")
> }
> 
> fileprivate func makeCallArgumentList(from params: FunctionParameterListSyntax, indexSeed: Int) -> String {
> var parts: [String] = []
> var counter = indexSeed
> for p in params {
> let label = p.firstName.text
> let varName = (p.secondName ?? p.firstName)?.text ?? "arg\(counter)"; counter += 1
> if label == "_" {
> parts.append(varName)
> } else {
> parts.append("\(label): \(varName)")
> }
> }
> return parts.joined(separator: ", ")
> }
> 
> fileprivate func makeCrashingDefaultInit(for methods: [FunctionDeclSyntax], facadeName: String) -> String {
> let arms = methods.map { fn -> String in
> let fnName = fn.name.text
> let params = makeClosureParamSignature(from: fn.signature.parameterClause.parameters, indexSeed: 0)
> let asyncTok = fn.signature.effectSpecifiers?.asyncSpecifier?.text ?? ""
> let throwsTok = fn.signature.effectSpecifiers?.throwsClause?.throwsSpecifier.text ?? ""
> let ret = fn.signature.returnClause?.type.description.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Void"
> let body = throwsTok.isEmpty ? "fatalError(\"\\(String(describing: \(facadeName).self)).\(fnName) is unimplemented\")"
> : "throw NSError(domain: \"Unimplemented\", code: -1)"
> return "\(fnName): { (\(params)) \(asyncTok) \(throwsTok) -> \(ret) in \(body) }"
> }
> return "\(facadeName)(\n \(arms.joined(separator: ",\n "))\n )"
> }
> 
> fileprivate func makePreviewDefaultInit(for methods: [FunctionDeclSyntax], facadeName: String) -> String {
> let arms = methods.map { fn -> String in
> let fnName = fn.name.text
> let params = makeClosureParamSignature(from: fn.signature.parameterClause.parameters, indexSeed: 0)
> let asyncTok = fn.signature.effectSpecifiers?.asyncSpecifier?.text ?? ""
> let throwsTok = fn.signature.effectSpecifiers?.throwsClause?.throwsSpecifier.text ?? ""
> let ret = fn.signature.returnClause?.type.description.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Void"
> let placeholderReturn: String = {
> if ret == "Void" { return "" }
> return "return (\(ret)).self is ExpressibleByNilLiteral.Type ? nil : { fatalError(\"Provide preview for \(fnName)\") }()"
> }()
> return "\(fnName): { (\(params)) \(asyncTok) \(throwsTok) -> \(ret) in \(placeholderReturn) }"
> }
> return "\(facadeName)(\n \(arms.joined(separator: ",\n "))\n )"
> }
> 
> ```

Thanks,

---

<div class="post-metadata">

**Author:** ![lsb](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/lsb/32/27798_2.png) [@lsb](https://forums.swift.org/u/lsb)\
**Post date:** [August 28, 2025, 5:28pm UTC](https://forums.swift.org/t/customization-and-namespacing-for-swift-openapi-generator-generated-code/81856/2 "2025-08-28T17:28:28Z")

</div>

That looks like a fairly complicated way to deal with a simple problem. My current way to do this is to just use separate target for separate OpenAPI clients.

---

<div class="post-metadata">

**Author:** ![mackoj](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mackoj/32/42395_2.png) [@mackoj](https://forums.swift.org/u/mackoj)\
**Post date:** [August 28, 2025, 7:10pm UTC](https://forums.swift.org/t/customization-and-namespacing-for-swift-openapi-generator-generated-code/81856/3 "2025-08-28T19:10:11Z")

</div>

Effectively, your approach handles name collisions if everything is internal, but it does not handle customization such as adding prefixes, suffixes, or macros to the generated code.

---

<div class="post-metadata">

**Author:** ![Honza\_Dvorsky](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/honza_dvorsky/32/29867_2.png) [@Honza\_Dvorsky](https://forums.swift.org/u/Honza_Dvorsky)\
**Post date:** [August 28, 2025, 7:57pm UTC](https://forums.swift.org/t/customization-and-namespacing-for-swift-openapi-generator-generated-code/81856/4 "2025-08-28T19:57:31Z")

</div>

Hi @mackoj,

thanks for the details of what you're trying to achieve. For the naming collisions, I'd echo @lsb’s suggestion of separate modules, and you can set the `accessModifier: package` in the config file, allowing you to consume both clients in a single file. You'd import both modules, one for each client, and where needed, use the fully qualified name, such as `GitHubClient.Client`.

Now, for attaching macros, that's not easily doable today, but we're interested in offering this feature. There truly are very useful things that it'd unlock, if the generator can simply include specific macros on specific generated types. We have an issue tracking it here, feel free to chime in there, or we can even provide guidance if you'd be interested in contributing this feature!

> <https://github.com/apple/swift-openapi-generator/issues/383>
>
> It would be nice to be able to add macros to the generated Schema and API reques…ts 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

---

<div class="post-metadata">

**Author:** ![mackoj](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mackoj/32/42395_2.png) [@mackoj](https://forums.swift.org/u/mackoj)\
**Post date:** [September 30, 2026, 8:16pm UTC](https://forums.swift.org/t/customization-and-namespacing-for-swift-openapi-generator-generated-code/81856/5 "2026-09-30T20:16:45Z")

</div>

Hi Honza, sorry to revive this thread. Following up on your suggestion, I opened a PR that lets you attach macros to generated code [Add macroAnnotations config option to attach attributes to generated types by mackoj · Pull Request #960 · apple/swift-openapi-generator · GitHub](https://github.com/apple/swift-openapi-generator/pull/960) .

It adds a macroAnnotations key to the config file. You can target specific schemas, all schemas with `*`, or the generated Client. The server side and generated methods aren't covered yet; I can add them if the approach works for you.

Any feedback is welcome, thanks!
