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

Merged
merged 36 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
68ef99c
Allow substituting types
Apr 24, 2025
762fbdd
Support replacement also in inline defined schemas
May 14, 2025
cff5760
More tests
May 14, 2025
0227843
WIP Proposal
May 14, 2025
8fe84b7
Add Example Project
May 14, 2025
8646df8
Apply x-swift-open-api-replace-type extension
May 14, 2025
37b8544
Add additionalProperties to examples
May 16, 2025
dbf467d
Replace in additionalProperties
May 16, 2025
108386a
Update ProposalText
May 16, 2025
715b35d
Remove vendor-extensions implementation
May 19, 2025
4922fa7
Add example
May 19, 2025
af6c242
more tests
May 19, 2025
2130376
Update Proposal Document
May 19, 2025
f14f710
add type-overrides in nested config
May 25, 2025
5753bc1
Apply suggestions from code review
simonbility May 26, 2025
33da4ee
fix failing test
May 26, 2025
b4acd4c
add doc comment
May 27, 2025
46b20fd
Update example code
May 27, 2025
11fa2c2
Remove Proposal added in separate PR
May 27, 2025
557e01a
Cleanup example project
Jun 10, 2025
7956564
emit warning when overriding non-existing schema
Jun 10, 2025
b2ea3fe
Switch to plugin in example
Jun 10, 2025
16f4cf0
Apply suggestions from code review
simonbility Jun 16, 2025
3df13a7
move validation logic
Jun 16, 2025
34aa46a
simplify run description
Jun 16, 2025
384f427
Migrate to SnippedBasedReferenceTests
Jun 16, 2025
31d11d0
test validation logic
Jun 16, 2025
c57b645
undo whitespace change
Jun 16, 2025
60595b9
Add TypeOverrides in Tutorials
Jun 16, 2025
e43134f
add todo for example project
Jun 16, 2025
73cc059
Update Examples/type-overrides-example/Sources/openapi.yaml
czechboy0 Jun 17, 2025
d4f8103
Merge branch 'main' into type-substitutions
czechboy0 Jun 17, 2025
f863db1
cleanup
Jun 17, 2025
7a271e5
Add License Header
Jun 17, 2025
f9418f3
Format Code
Jun 17, 2025
88706b7
Format Code in Tests
Jun 17, 2025
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
// 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)],
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.

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

}

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'
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'


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
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
/// - 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(),
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] {
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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FWIW i just ran the code-formatter on this file.

/// 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 {
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")
)
}
}
}
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.
public struct TypeOverrides: Sendable {
/// A dictionary of overrides for replacing the types generated from schemas with manually provided types
public var schemas: [String: String]

/// Creates a new instance of `TypeOverrides`
/// - Parameter schemas: A dictionary mapping schema names to their override types.
public init(schemas: [String: String] = [:]) { self.schemas = schemas }

/// A Boolean value indicating whether there are no overrides.
public var isEmpty: Bool { schemas.isEmpty }
}
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 = """

- 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.
/// - Parameter config: The configuration specified 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()
}

/// 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 {

/// A dictionary of overrides for replacing the types generated from schemas with manually provided types
var schemas: [String: String]?
}
}
Loading