Skip to content

Update OpenAPIKit to v6#875

Merged
simonjbeaumont merged 26 commits intoapple:mainfrom
mattpolzin:update-openapi-kit-to-v5
May 5, 2026
Merged

Update OpenAPIKit to v6#875
simonjbeaumont merged 26 commits intoapple:mainfrom
mattpolzin:update-openapi-kit-to-v5

Conversation

@mattpolzin
Copy link
Copy Markdown
Contributor

@mattpolzin mattpolzin commented Mar 9, 2026

This PR builds upon #839 to update from OpenAPIKit 4.x to OpenAPIKit 5.0 6.0.

Motivation

Updating to OpenAPIKit v4.x enables efforts to add external loading support to the generator (as a separate future project). This means being able to read in OpenAPI Documents spread across multiple files.

Updating to OpenAPIKit v5.x allows the generator project to support OAS 3.2.0 features and take advantage of various other improvements to OpenAPIKit that required breaking changes to be made.

Updating to OpenAPIKit v6.0 allows this project to turn the ExternalLoading trait off (on by default). Turning this trait off saves 50% of the compile time of OpenAPKit for as long as swift-openapi-generator is not using the external loading features of OpenAPKit anyway.

Modifications

  • Update OpenAPIKit dependency to v6.x
  • Remove code that parsed OAS 3.2.0 documents as 3.1.x documents; OpenAPIKit v5 supports OAS 3.2.0 documents natively.
  • Make changes required by move to new OpenAPIKit version (following migration guide).
  • Introduce assumeLookupOnce() to temporarily maintain the existing invariant that no entries in the Components Object are themselves references; replace all lookup() calls with assumeLookupOnce(). See the Note on this below.
  • Fix code that looked for references on JSON Schemas under the assumption that they were wrapped in OpenAPI.Reference. This assumption does not hold in OpenAPIKit v5.

NOTE on assumeLookupOnce()
The new assumeLookupOnce() function is intended to allow for gradual migration. The generator had previously used lookup() which used to look at the Components Object and either return an entry or throw. Going forward, lookup() does recursive lookup until a non-reference is found and lookupOnce() acts more like lookup() used to act -- however, lookupOnce() has a different method signature owing to the fact that it may return a reference instead of the ultimately desired object. The need for these changes arise from the fact that OpenAPIKit v5 now allows entries in the Components Object to themselves be references. This has been supported by the OpenAPI Standard for some time but OpenAPIKit only introduced support for it with v5.

Therefore, assumeLookupOnce() was added as a way to throw an error if a reference is found as an entry in the Components Object (behavior already exhibited by the generator project) but the goal should be to remove uses of assumeLookupOnce() over time and replace them with either lookupOnce() or lookup() depending on the needs of each call site. In this way, the generator project will move closer to supporting Component Object reference entries without the overhead of changing every call site all at once.

Result

Theoretically, nothing functionally changes.

Test Plan

Tests updated, swift test passing, "Compatibility test" suite passing.

@mattpolzin mattpolzin force-pushed the update-openapi-kit-to-v5 branch 3 times, most recently from b61dbf0 to 4e5ff88 Compare March 10, 2026 14:37
@mattpolzin mattpolzin changed the title Update openapi kit to v5 Update OpenAPIKit to v5 Mar 11, 2026
@mattpolzin mattpolzin force-pushed the update-openapi-kit-to-v5 branch from 4e5ff88 to 7b35cbd Compare March 11, 2026 13:46
iT0ny and others added 12 commits March 27, 2026 17:56
Co-authored-by: Mathew Polzin <matt.polzin@gmail.com>
…g OAS documents prior to 3.2.0 in the same manner for now
… that no Components Objects entries are references
This could be squashed into 'fix code that inspects UnresolvedSchemas
for references' except for the fact that two commits tells the story
better. The first commit fixes by looking into JSONSchema.references
when they are found in UnresolvedSchemas in places where that is needed.
This second commit cleans up by flattening the UnresolvedSchema so that
we only need the inner of the two switch statements.
@mattpolzin mattpolzin force-pushed the update-openapi-kit-to-v5 branch from 7b35cbd to 2113489 Compare March 27, 2026 15:58
@mattpolzin mattpolzin marked this pull request as ready for review April 1, 2026 15:12
Comment thread Package.swift Outdated
Comment on lines +103 to +105
// Also handle nil values in nullable schemas.
let isNullValue = anyValue is Void || (anyValue as? String) == nil
if isNullable && isNullValue {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this new check correct? Won't (anyValue as? String) be nil for non-nullable non-string values?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm taking a closer look at this now. I inherited this change from the v4 bump.

non-nullable

From what I can tell, this possibility is ruled out because isNullable must be true on line 105.

non-string

We are in a case where the "backing type" has been determined to be a string, so I imagine that was the thinking with assuming the allowed values would be strings. I can say for sure that if the JSON Schema is of string type, then OpenAPIKit will special-case its parsing of allowedValues to only parse String (or String? if nullable) values. Tracing the calls in the generator project for translateRawEnum() I see that it does only pass .string as the backing type when OpenAPIKit has determined the schema type to be string, so this change actually should be safe and correct, albeit a little involved to sanity check!

@mattpolzin
Copy link
Copy Markdown
Contributor Author

Thanks for the review. I'll fix the CI failures in follow-up commits (formatting fixed just now).

Specifically,
Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateComponents.swift:38:54: error: generic parameter 'T' could not be inferred
@simonjbeaumont
Copy link
Copy Markdown
Collaborator

@mattpolzin I've merged #887 now and updated this PR branch so 6.0 is no longer an issue.

The fact the unit tests are not completing appears to be actually relevant to this PR as all the tests passed fine in #887.

@mattpolzin
Copy link
Copy Markdown
Contributor Author

Troubling. This represents a massive performance regression. The examples CI jobs that do succeed before timing out still take close to twice as long on this branch as they did on the branch you linked to.

Since it all compiles and works, I think I'll need to just make educated guesses at what might be slowing the compiler down and tweak those things.

@mattpolzin
Copy link
Copy Markdown
Contributor Author

mattpolzin commented Apr 16, 2026

There's something really subtle going on here that I have not figured out yet. In the most recent CI run, Linux-based "Release Build" for Swift 6.2 finished in 5m 40s but for Swift 6.1 it timed out.

What's worse, I can't reproduce any of this on my Linux laptop inside a docker container (yet). Using a Swift 6.1 Docker Image I saw a swift build of this branch take 1.5 minutes and swift test took under 30s. But I am not sure I am correctly reproducing what is happening for the "Unit test" CI workflows because they are fairly complex (based on several layers of YAML configurations) and they may not in fact be analogous to swift test.

@mattpolzin
Copy link
Copy Markdown
Contributor Author

I 'd like to come up with a troubleshooting process that doesn't involve me pushing a hopeful change and seeing if it works after CI runs in GitHub.

@mattpolzin
Copy link
Copy Markdown
Contributor Author

mattpolzin commented Apr 16, 2026

Well, I've found something to go on I think. As a proxy for what's going on in CI (though the situation is not nearly as severe on my personal laptop) I can observe a meaningful difference in compile time between the main branch and this one when building within a Swift docker container.

Interestingly, over half the difference in build time can be observed going as far back as the bump to OpenAPIKit v4, the other half (or less) comes from bumping to OpenAPKit v5.

I'm worried this may have less to do with function overloading slowing the compilation down and more to do with the introduction of concurrency (mainly Sendable) support into OpenAPKit given that realization. Still more digging to do, but that's all I've got time for at the moment.

@simonjbeaumont
Copy link
Copy Markdown
Collaborator

@mattpolzin yeah, there's a bunch of abstractions in the CI that come from our use of a shared set of GitHub Workflows configs from another repo and the use of matrix jobs. But they really should boil down to just swift test at the end.

Worth noting that the compatibility tests require an environment variable to enable: SWIFT_OPENAPI_COMPATIBILITY_TEST_ENABLE=true swift test. They're interesting because they create a little harness with a real OpenAPI doc and go through the same steps as an adopter build (more or less).

Hopefully you have enough to work through locally. I wonder if it's worth having a look at some of the compiler flags, like -warn-long-expression-type-checking or -profile-stats-entities?

Would be good if we can understand the impact here and then we can assess the blast radius. I don't think CI should be the issue, but if we can identify a pathological setup in CI that isn't representative of users then maybe we can change CI.

@mattpolzin
Copy link
Copy Markdown
Contributor Author

This is not very promising news I'm afraid. I have bisected the performance degradation and the vast majority of it occurs between OpenAPIKit versions 4.0.0-alpha.4 and 4.0.0-alpha.5. This makes me 98% confident that the reason Swift compilation is taking longer is almost entirely due to Swift Concurrency features. Most likely specifically async let uses in OpenAPIKit's external dereferencing code.

The numbers (on my personal laptop) of swift build of this project are:

OpenAPIKit Version User Time Real Time
v4.0.0-alpha.4 5m24s 0m35s
v4.0.0-alpha.5 8m29s 0m53s
v5.3.0 9m51s 1m9s

On the plus side, compilation does not take very long given swift build is run on a modern CPU with multiple cores available (Real Time above feels not terrible even though it is much worse than it was before concurrency code was added).

I'm not confident any good solution exists other than waiting for Swift to compile faster or removing concurrency (which I added to the external dereferencing code by request of Honza; and, it is definitely a good use-case for concurrency).


Any thoughts based on the above?

@mattpolzin
Copy link
Copy Markdown
Contributor Author

mattpolzin commented Apr 17, 2026

My first thought was add more type annotations. Annotating the async let lines with their types does not make any difference it doesn't look like.

My next thought is conditional compilation -- at least until or unless a project wants to use OpenAPIKit to perform external dereferencing, the massive hit to compilation time may be avoidable. I'll experiment with that at least enough to know if it makes much of a difference like I think it might.

@mattpolzin
Copy link
Copy Markdown
Contributor Author

Good news/Bad news.

I added a trait that conditionally compiles the external dereferencing code and that was entirely the slowdown.

ExternalLoading trait User Compile Time Real Compile Time
enabled 9m58s 1m9s
disabled 5m15s 0m31s

The bad news is that package traits are only available for Swift 6.1+ so to disable external loading we will need to bump swift-openapi-generator to OpenAPIKit v6, not v5. In v6 I will bump minimum Swift version to 6.1 and add the new trait.

The other bad news, naturally, is that Swift compilation of concurrency features is so slow that it might be considered prohibitive to enable an OpenAPIKit feature that would allow swift-openapi-generator to load external OpenAPI references.

@simonjbeaumont
Copy link
Copy Markdown
Collaborator

Oof. Thanks for the update, Matt! IIUC that means we have an incremental path forward. Swift OpenAPI Generator already is 6.1+ so we could take on a trait when bumping the OpenAPIKit dependency.

I agree it's a shame it won't be as straightforward to investigate support for external references. We could explore using traits in Swift OpenAPI Generator too if the cost is prohibitively high to enable by default?

@mattpolzin
Copy link
Copy Markdown
Contributor Author

It'll be on by default in OpenAPIKit because I don't want to assume that any given project will be driven by a desire to keep compilation time low. But I'll disable it when bringing v6 of OpenAPIKit into the generator project because that'll make it much easier to justify the update (CI won't take substantially longer or time out for you).

I figure then it's up to y'all when you are ready to sink time into external loading features whether the compilation time cost is appropriate or not.

@mattpolzin mattpolzin changed the title Update OpenAPIKit to v5 Update OpenAPIKit to ~v5~ v6 Apr 29, 2026
@mattpolzin mattpolzin changed the title Update OpenAPIKit to ~v5~ v6 Update OpenAPIKit to v6 Apr 29, 2026
@mattpolzin
Copy link
Copy Markdown
Contributor Author

Alright, I've bumped to OpenAPIKit v6 now. Major version 6 does not involve any breaking changes. It bumps the minimum Swift version to 6.1 (already this project's minimum requirement) and it encloses external loading features in an ExternalLoading package trait (on by default, but turned off for swift-openapi-generator for now to save a lot on compile time).

@mattpolzin
Copy link
Copy Markdown
Contributor Author

@simonjbeaumont looks like there's some sort of caching issue in CI maybe? The "Examples" job for Swift 6.1 succeeds (resolving OpenAPIKit to version 6.0.0) but the Examples job for Swift 6.2 fails (resolving OpenAPIKit to version 3.9.0).

@simonjbeaumont
Copy link
Copy Markdown
Collaborator

Looking at the examples CI, this looks to be because the examples share a common harness and, on each example, do a swift package unedit ... || swift package edit ..., with the || there to catch the first time it's not in edit mode. Both of these commands include --skip-update which I think is the main problem. When we swift package edit in the swift-openapi-generator branch for this PR it doesn't update the dependencies, which clearly it needs to. Then it tries to declare the dependency on OpenAPIKit disabling traits, but the cached version had no traits, which is a SwiftPM error.

Let me see if I can get a PR up to fix that.

simonjbeaumont added a commit that referenced this pull request May 5, 2026
…ed (#902)

### Motivation

This project has CI for example projects and an integration test. These
use a standalone package with a dependency on
`swift-openapi-generator`. The scripts driving this CI used `swift
package edit` to override this dependency to the
checkout of the PR under test.

Unfortunately this does not work in the presence of package traits. In
open PR #875 the OpenAPIKit dependency is being
bumped and is switched to disabling default traits. However, the CI is
failing to perform the `swift package edit`
command. The root cause is that `swift package edit` performs dependency
resolution in order to edit the dependency.
This resolves OpenAPIKit to a version satisfying the constraints on main
-- a version of OpenAPIKit that has no traits.
After the override, the build fails because SwiftPM forbids disabling
traits on a package that has no declared traits.
This is because OpenAPIKit is not re-resolved to the version declared in
the local PR branch, even though it has been
bumped. After quite some investigation, it appears there's no way to
influence this behaviour or to defer resolution
until after the dependency edit. This thesis aligns with the open
SwiftPM issue tracking support for traits for `swift
package edit`[^1]

### Modifications

Workaround the lack of trait-aware `swift package edit` by updating the
examples and integration test CI scripts to
instead inject an override block into the package manifest.

### Result

The scripts should now work as intended, even when the minimum version
of a transitive dependency is bumped and/or when
transitive dependencies make use of package traits.

### Test Plan

Reproduced the original error in #875:

```console
% git rev-parse --symbolic-full-name HEAD
refs/heads/update-openapi-kit-to-v5

% SINGLE_EXAMPLE_PACKAGE=hello-world-urlsession-client-example ./scripts/test-examples.sh
...
error: Disabled default traits by package 'swift-openapi-generator' (swift-openapi-generator) on package 'openapikit' (OpenAPIKit) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break.
```

With this patch applied we can run the script fine on the PR branch:

```console
% git cherry-pick sb/fix-package-override-in-ci-scripts
[update-openapi-kit-to-v5 2544c13] Update CI scripts to support dependency overrides where traits are used
 Date: Tue May 5 14:55:35 2026 +0100
 2 files changed, 29 insertions(+), 18 deletions(-)

% SINGLE_EXAMPLE_PACKAGE=hello-world-urlsession-client-example ./scripts/test-examples.sh
...
** Overriding swift-openapi-generator dependency in hello-world-urlsession-client-example to use /Users/Si/work/code/swift-openapi-generator
** Building example package: hello-world-urlsession-client-example
...
Computing version for https://github.com/mattpolzin/OpenAPIKit
Computed https://github.com/mattpolzin/OpenAPIKit at 6.0.0 (14.10s)
...
Creating working copy for https://github.com/mattpolzin/OpenAPIKit
Working copy of https://github.com/mattpolzin/OpenAPIKit resolved at 6.0.0
...
Build complete! (44.22s)
** ✅ Successfully built the example package hello-world-urlsession-client-example.
```

[^1]: swiftlang/swift-package-manager#8354
@simonjbeaumont
Copy link
Copy Markdown
Collaborator

Thank you so much for driving this effort @mattpolzin and for your patience as we navigated through the changes. I think this is good to land now. ❤️ 🙏

@simonjbeaumont simonjbeaumont enabled auto-merge (squash) May 5, 2026 15:58
@simonjbeaumont simonjbeaumont merged commit d154389 into apple:main May 5, 2026
50 checks passed
@mattpolzin mattpolzin deleted the update-openapi-kit-to-v5 branch May 5, 2026 16:20
@simonjbeaumont
Copy link
Copy Markdown
Collaborator

Also—thank you to @iT0ny!

simonjbeaumont added a commit that referenced this pull request May 6, 2026
…903)

### Motivation

After updating to OpenAPIKit 6.0 (#875), the generator fails on OpenAPI
documents that use `style: simple` on query parameters. While this is
technically invalid per the OpenAPI specification, it was previously
tolerated -- the generator would skip the unsupported parameter with a
warning diagnostic and continue generation.

The new OpenAPIKit 6.0 brings a new default built-in validation
(`.parameterStyleAndLocationAreCompatible`) that rejects documents with
this combination at validation time. Because this fires as a validation
error (not a warning), it is thrown even with `strict: false`, before
the translator gets a chance to handle the parameter gracefully.

This is a regression for users with documents that have this common spec
violation, which we do have examples of in our extended compatibility
test suite).

### Modifications

Replace the use of `Validator()` (which includes all OpenAPIKit
defaults) with a custom validator built from `Validator.blank`. This
explicitly lists all default validations from OpenAPIKit 6.0.0 except
`.parameterStyleAndLocationAreCompatible`, restoring the previous
behaviour where the generator tolerates these documents.

We might want to revisit how lenient we are for this, but this patch is
the most expeditious way to getting a release out with the OpenAPIKit
update.

### Result

Documents with `style: simple` on query parameters are no longer
rejected at validation time. The translator continues to emit an
"unsupported" diagnostic and skip the parameter, as it did before the
OpenAPIKit upgrade:

```
'Feature "Query params of style simple, explode: false" is not supported, skipping'
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants