Skip to content

Allow substituting types #764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Examples/type-overrides-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.vscode
/Package.resolved
.ci/
.docc-build/
39 changes: 39 additions & 0 deletions Examples/type-overrides-example/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// swift-tools-version:5.10
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors

// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import PackageDescription

let package = Package(
name: "type-overrides-example",
platforms: [.macOS(.v14)],
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
platforms: [.macOS(.v14)],
platforms: [.macOS(.v10_15)],

I don't think we're using any new features in this example beyond the minimum platform version?

products: [
.library(name: "Types", targets: ["Types"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.6.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"),
],
targets: [
.target(
name: "Types",
dependencies: [
"ExternalLibrary",
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")],
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
),
.target(
name: "ExternalLibrary"
),
]
)
18 changes: 18 additions & 0 deletions Examples/type-overrides-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Overriding types

An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator).

> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only.

## Overview

This example shows how to use the TypeOverrides feature of the Swift OpenAPI Generator.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
This example shows how to use the TypeOverrides feature of the Swift OpenAPI Generator.
This example shows how to use [type overrides](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/configuring-the-generator) with Swift OpenAPI Generator.

Copy link
Contributor

Choose a reason for hiding this comment

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


## Usage

Build:

```console
% swift build
Build complete!
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

/// Example struct to be used instead of the default generated type.
/// This illustrates how to introduce a type performing additional validation during Decoding that cannot be expressed with OpenAPI
public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable {
public let rawValue: Int
public init?(rawValue: Int) {
if !rawValue.isPrime { return nil }
self.rawValue = rawValue
}

public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let number = try container.decode(Int.self)
guard let value = PrimeNumber(rawValue: number) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "The number is not prime.")
}
self = value
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.rawValue)
}

}
Comment on lines +2 to +24
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Example struct to be used instead of the default generated type.
/// This illustrates how to introduce a type performing additional validation during Decoding that cannot be expressed with OpenAPI
public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable {
public let rawValue: Int
public init?(rawValue: Int) {
if !rawValue.isPrime { return nil }
self.rawValue = rawValue
}
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let number = try container.decode(Int.self)
guard let value = PrimeNumber(rawValue: number) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "The number is not prime.")
}
self = value
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.rawValue)
}
}
/// Example struct to be used instead of the default generated type.
/// This illustrates how to introduce a type performing additional validation during Decoding that cannot be expressed with OpenAPI
public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable {
public let rawValue: Int
public init?(rawValue: Int) {
if !rawValue.isPrime { return nil }
self.rawValue = rawValue
}
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let number = try container.decode(Int.self)
guard let value = PrimeNumber(rawValue: number) else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "The number is not prime.")
}
self = value
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.rawValue)
}
}

Tiny nits


extension Int {
fileprivate var isPrime: Bool {
if self <= 1 { return false }
if self <= 3 { return true }

var i = 2
while i * i <= self {
if self % i == 0 { return false }
i += 1
}
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generate:
- types
accessModifier: package
namingStrategy: idiomatic
additionalImports:
- Foundation
- ExternalLibrary
typeOverrides:
schemas:
UUID: Foundation.UUID
PrimeNumber: ExternalLibrary.PrimeNumber
1 change: 1 addition & 0 deletions Examples/type-overrides-example/Sources/Types/openapi.yaml
46 changes: 46 additions & 0 deletions Examples/type-overrides-example/Sources/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
openapi: '3.1.0'
info:
title: GreetingService
version: 1.0.0
servers:
- url: https://example.com/api
description: Example service deployment.
paths:
/user:
get:
operationId: getUser
parameters:
- name: name
required: false
in: query
description: The name of the user
schema:
type: string
responses:
'200':
description: A success response with the user.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
Comment on lines +8 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
paths:
/user:
get:
operationId: getUser
parameters:
- name: name
required: false
in: query
description: The name of the user
schema:
type: string
responses:
'200':
description: A success response with the user.
content:
application/json:
schema:
$ref: '#/components/schemas/User'

Let's drop all of paths since we're not generating client/server.

components:
schemas:
UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in open-api-generator-config
type: string
format: uuid

PrimeNumber: # this will be replaced by with ExternalLibrary.PrimeNumber specified by typeOverrides in open-api-generator-config
type: string
format: uuid

User:
type: object
properties:
id:
$ref: '#/components/schemas/UUID'
name:
type: string
favorite_prime_number:
$ref: '#/components/schemas/PrimeNumber'


Comment on lines +45 to +46
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

6 changes: 6 additions & 0 deletions Sources/_OpenAPIGeneratorCore/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public struct Config: Sendable {

/// A map of OpenAPI identifiers to desired Swift identifiers, used instead of the naming strategy.
public var nameOverrides: [String: String]

/// A map of OpenAPI paths to desired Types
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// A map of OpenAPI paths to desired Types
/// A map of OpenAPI schema names to desired custom type names.

public var typeOverrides: TypeOverrides

/// Additional pre-release features to enable.
public var featureFlags: FeatureFlags
Expand All @@ -73,6 +76,7 @@ public struct Config: Sendable {
/// Defaults to `defensive`.
/// - nameOverrides: A map of OpenAPI identifiers to desired Swift identifiers, used instead
/// of the naming strategy.
/// - typeOverrides: A map of OpenAPI paths to desired Types
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// - typeOverrides: A map of OpenAPI paths to desired Types
/// - typeOverrides: A map of OpenAPI schema names to desired custom type names.

/// - featureFlags: Additional pre-release features to enable.
public init(
mode: GeneratorMode,
Expand All @@ -81,6 +85,7 @@ public struct Config: Sendable {
filter: DocumentFilter? = nil,
namingStrategy: NamingStrategy,
nameOverrides: [String: String] = [:],
typeOverrides: TypeOverrides = TypeOverrides(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
typeOverrides: TypeOverrides = TypeOverrides(),
typeOverrides: TypeOverrides = .init(),

featureFlags: FeatureFlags = []
) {
self.mode = mode
Expand All @@ -89,6 +94,7 @@ public struct Config: Sendable {
self.filter = filter
self.namingStrategy = namingStrategy
self.nameOverrides = nameOverrides
self.typeOverrides = typeOverrides
self.featureFlags = featureFlags
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//
import OpenAPIKit
import Foundation

extension TypesFileTranslator {

Expand Down Expand Up @@ -87,6 +88,15 @@ extension TypesFileTranslator {
)
)
}
if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides.schemas[jsonPath] {
Comment on lines 90 to +91
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}
if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides.schemas[jsonPath] {
}
// Apply type overrides.
if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides.schemas[jsonPath] {

let typeOverride = TypeName(swiftKeyPath: typeOverride.components(separatedBy: "."))
let typealiasDecl = try translateTypealias(
named: typeName,
userDescription: overrides.userDescription ?? schema.description,
to: typeOverride.asUsage
)
return [typealiasDecl]
}

// If this type maps to a referenceable schema, define a typealias
if let builtinType = try typeMatcher.tryMatchReferenceableType(for: schema, components: components) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,6 @@ enum Constants {
/// The substring used in method names for the multipart coding strategy.
static let multipart: String = "Multipart"
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Please undo this change

/// Constants related to types used in many components.
enum Global {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ extension TypesFileTranslator {
_ schemas: OpenAPI.ComponentDictionary<JSONSchema>,
multipartSchemaNames: Set<OpenAPI.ComponentKey>
) throws -> Declaration {
try diagnoseTypeOverrideForNonExistentSchema()

let decls: [Declaration] = try schemas.flatMap { key, value in
try translateSchema(
Expand All @@ -76,4 +77,18 @@ extension TypesFileTranslator {
)
return componentsSchemasEnum
}
private func diagnoseTypeOverrideForNonExistentSchema() throws {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a great idea - please just move it to validateDoc.swift and add doc comments, and invoke it from validateDoc, rather than from translateSchemas. Thanks!

let nonExistentOverrides = config.typeOverrides.schemas.keys
.filter { key in
guard let componentKey = OpenAPI.ComponentKey(rawValue: key) else { return false }
return !self.components.schemas.contains(key: componentKey)
}
.sorted()

for override in nonExistentOverrides {
try diagnostics.emit(
.warning(message: "TypeOverride defined for schema '\(override)' that is not defined in the OpenAPI document")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.warning(message: "TypeOverride defined for schema '\(override)' that is not defined in the OpenAPI document")
.warning(message: "A type override defined for schema '\(override)' is not defined in the OpenAPI document.")

)
}
}
}
26 changes: 26 additions & 0 deletions Sources/_OpenAPIGeneratorCore/TypeOverrides.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// Type containing overrides for schema types.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Type containing overrides for schema types.
/// A container of schema type overrides.

public struct TypeOverrides: Sendable {
/// A dictionary of overrides for replacing the types generated from schemas with manually provided types
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// A dictionary of overrides for replacing the types generated from schemas with manually provided types
/// A dictionary of overrides for replacing named schemas from the OpenAPI document with custom types.

public var schemas: [String: String]

/// Creates a new instance of `TypeOverrides`
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Creates a new instance of `TypeOverrides`
/// Creates a new instance.

/// - Parameter schemas: A dictionary mapping schema names to their override types.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// - Parameter schemas: A dictionary mapping schema names to their override types.
/// - Parameter schemas: A dictionary of overrides for replacing named schemas from the OpenAPI document with custom types.

public init(schemas: [String: String] = [:]) { self.schemas = schemas }

/// A Boolean value indicating whether there are no overrides.
public var isEmpty: Bool { schemas.isEmpty }
Comment on lines +23 to +25
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// A Boolean value indicating whether there are no overrides.
public var isEmpty: Bool { schemas.isEmpty }

I don't think we need this?

}
10 changes: 10 additions & 0 deletions Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension _GenerateOptions {
let resolvedAdditionalImports = resolvedAdditionalImports(config)
let resolvedNamingStragy = resolvedNamingStrategy(config)
let resolvedNameOverrides = resolvedNameOverrides(config)
let resolvedTypeOverrides = resolvedTypeOverrides(config)
let resolvedFeatureFlags = resolvedFeatureFlags(config)
let configs: [Config] = sortedModes.map {
.init(
Expand All @@ -43,11 +44,19 @@ extension _GenerateOptions {
filter: config?.filter,
namingStrategy: resolvedNamingStragy,
nameOverrides: resolvedNameOverrides,
typeOverrides: resolvedTypeOverrides,
featureFlags: resolvedFeatureFlags
)
}
let (diagnostics, finalizeDiagnostics) = preparedDiagnosticsCollector(outputPath: diagnosticsOutputPath)
let doc = self.docPath
let typeOverridesDescription = """

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

Copy link
Author

Choose a reason for hiding this comment

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

This one actually was on purpose.
The intent was to add a linebreak after TypeOverrides and then have all the overrides (only schemas at the moment) indented under it.

- TypeOverrides:<-this is the linebreak
  - Schemas: "Old"->"New" 

Without it would look like this

- TypeOverrides:  - Schemas: "Old"->"New" 

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's make it even shorter, - Type overrides: "Foo" -> "Bar", "Baz" -> "UUID", all one line.

- Schemas: \(resolvedTypeOverrides.schemas.isEmpty ? "<none>" : resolvedTypeOverrides.schemas
.sorted(by: { $0.key < $1.key })
.map { "\"\($0.key)\"->\"\($0.value)\"" }
.joined(separator: ", "))
"""
print(
"""
Swift OpenAPI Generator is running with the following configuration:
Expand All @@ -59,6 +68,7 @@ extension _GenerateOptions {
- Name overrides: \(resolvedNameOverrides.isEmpty ? "<none>" : resolvedNameOverrides
.sorted(by: { $0.key < $1.key })
.map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", "))
- Type overrides: \(resolvedTypeOverrides.isEmpty ? "<none>" : typeOverridesDescription)
- Feature flags: \(resolvedFeatureFlags.isEmpty ? "<none>" : resolvedFeatureFlags.map(\.rawValue).joined(separator: ", "))
- Output file names: \(sortedModes.map(\.outputFileName).joined(separator: ", "))
- Output directory: \(outputDirectory.path)
Expand Down
7 changes: 7 additions & 0 deletions Sources/swift-openapi-generator/GenerateOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ extension _GenerateOptions {
/// - Parameter config: The configuration specified by the user.
/// - Returns: The name overrides requested by the user
func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] { config?.nameOverrides ?? [:] }
/// Returns the type overrides requested by the user.
Comment on lines 95 to +96
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] { config?.nameOverrides ?? [:] }
/// Returns the type overrides requested by the user.
func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] { config?.nameOverrides ?? [:] }
/// Returns the type overrides requested by the user.

/// - Parameter config: The configuration specified by the user.
/// - Returns: The type overrides requested by the user
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// - Returns: The type overrides requested by the user
/// - Returns: The type overrides requested by the user.

func resolvedTypeOverrides(_ config: _UserConfig?) -> TypeOverrides {
if let typeOverrides = config?.typeOverrides { return TypeOverrides(schemas: typeOverrides.schemas ?? [:]) }
return TypeOverrides()
}
Comment on lines +99 to +102
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func resolvedTypeOverrides(_ config: _UserConfig?) -> TypeOverrides {
if let typeOverrides = config?.typeOverrides { return TypeOverrides(schemas: typeOverrides.schemas ?? [:]) }
return TypeOverrides()
}
func resolvedTypeOverrides(_ config: _UserConfig?) -> TypeOverrides {
guard let schemaOverrides = config?.typeOverrides?.schemas, !schemaOverrides.isEmpty else {
return .init()
}
return TypeOverrides(schemas: schemaOverrides)
}


/// Returns a list of the feature flags requested by the user.
/// - Parameter config: The configuration specified by the user.
Expand Down
10 changes: 10 additions & 0 deletions Sources/swift-openapi-generator/UserConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ struct _UserConfig: Codable {
/// Any names not included use the `namingStrategy` to compute a Swift name.
var nameOverrides: [String: String]?

/// A dictionary of overrides for replacing the types of generated with manually provided types
var typeOverrides: TypeOverrides?

/// A set of features to explicitly enable.
var featureFlags: FeatureFlags?

Expand All @@ -54,6 +57,13 @@ struct _UserConfig: Codable {
case filter
case namingStrategy
case nameOverrides
case typeOverrides
case featureFlags
}

struct TypeOverrides: Codable {
Comment on lines +63 to +64
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
struct TypeOverrides: Codable {
/// A container of type overrides.
struct TypeOverrides: Codable {


/// A dictionary of overrides for replacing the types generated from schemas with manually provided types
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// A dictionary of overrides for replacing the types generated from schemas with manually provided types
/// A dictionary of overrides for replacing the types generated from schemas with manually provided types.

var schemas: [String: String]?
}
}
Loading