Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
70f60d0
Bump OpenAPIKit version
iT0ny Oct 23, 2025
29b994e
Update OpenAPI.Content.Encoding usage according to OpenAPIKit v4 Migr…
iT0ny Oct 23, 2025
5911250
Update wording for `testEmitsComplexOpenAPIParsingError` and `testSch…
iT0ny Oct 23, 2025
62fe491
Update checks for null values
iT0ny Oct 23, 2025
0aa0187
Bump OpenAPIKit to 4.3.1 or greater
iT0ny Oct 28, 2025
acada4d
Fix "Compatibility test" CI suite
iT0ny Mar 27, 2026
5827f75
Merge branch 'main' into update-openapi-kit-to-v4
iT0ny Mar 27, 2026
dfec3ee
Update OpenAPIKit to v5
mattpolzin Mar 9, 2026
eec4ef0
remove pre-support for v3.2.0 OpenAPI Documents. these are now suppor…
mattpolzin Mar 9, 2026
6220411
replace content.encoding with content.encodingMap to continue handlin…
mattpolzin Mar 9, 2026
2436218
fix exhaustive checks on parameter location
mattpolzin Mar 9, 2026
7d7527c
fix Content and Content Map code
mattpolzin Mar 9, 2026
a389607
fix component entry lookup
mattpolzin Mar 9, 2026
9c9d5af
introduce assumeLookupOnce temporarily to maintain existing invariant…
mattpolzin Mar 10, 2026
63e789c
fix code that inspects UnresolvedSchemas for references
mattpolzin Mar 10, 2026
38bc15c
cleanup/simlipfy after fixing UnresolvedSchema lookups
mattpolzin Mar 11, 2026
2113489
test fixes
mattpolzin Mar 9, 2026
e995adc
swift format
mattpolzin Apr 13, 2026
e146521
Address Swift 6 on Linux type inference failure
mattpolzin Apr 13, 2026
59478d6
undo Yams minimum version bump
mattpolzin Apr 13, 2026
ed59e8f
Merge branch 'main' into update-openapi-kit-to-v5
simonjbeaumont Apr 15, 2026
1a7ac3c
Merge branch 'main' into update-openapi-kit-to-v5
simonjbeaumont Apr 16, 2026
f4ae36f
bump to OpenAPIKit 6
mattpolzin Apr 29, 2026
0cfc5db
Merge branch 'main' into update-openapi-kit-to-v5
mattpolzin Apr 29, 2026
d421454
Merge branch 'main' into update-openapi-kit-to-v5
mattpolzin May 1, 2026
ea09784
Merge branch 'main' into update-openapi-kit-to-v5
simonjbeaumont May 5, 2026
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
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ let package = Package(
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),

// Read OpenAPI documents
.package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.9.0"),
.package(url: "https://github.com/jpsim/Yams", "4.0.0"..<"7.0.0"),
.package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "5.1.1"),
.package(url: "https://github.com/jpsim/Yams", "5.1.0"..<"7.0.0"),
Comment thread
mattpolzin marked this conversation as resolved.
Outdated

// CLI Tool
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
Expand Down
26 changes: 25 additions & 1 deletion Sources/_OpenAPIGeneratorCore/Extensions/OpenAPIKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,36 @@ extension Either {
/// - Throws: An error if there's an issue looking up the value in the components.
func resolve(in components: OpenAPI.Components) throws -> B where A == OpenAPI.Reference<B> {
switch self {
case let .a(a): return try components.lookup(a)
case let .a(a): return try components.assumeLookupOnce(a)
case let .b(b): return b
}
}
}

extension OpenAPI.Components {
func assumeLookupOnce<ReferenceType: ComponentDictionaryLocatable>(_ reference: OpenAPI.Reference<ReferenceType>) throws -> ReferenceType{
guard let result = try lookupOnce(reference).b else {
throw JSONReferenceParsingError.componentsReferenceEntryUnsupported(reference.absoluteString)
}
return result
}

func assumeLookupOnce<ReferenceType: ComponentDictionaryLocatable>(_ reference: JSONReference<ReferenceType>) throws -> ReferenceType{
guard let result = try lookupOnce(reference).b else {
throw JSONReferenceParsingError.componentsReferenceEntryUnsupported(reference.absoluteString)
}
return result
}

func assumeLookupOnce<ReferenceType: ComponentDictionaryLocatable>(_ maybeReference: Either<OpenAPI.Reference<ReferenceType>, ReferenceType>) throws -> ReferenceType{
guard let result = try lookupOnce(maybeReference).b else {
throw JSONReferenceParsingError.componentsReferenceEntryUnsupported(maybeReference.a?.absoluteString)
}
return result
}

}

extension JSONSchema.Schema {

/// Returns the name of the schema.
Expand Down
60 changes: 27 additions & 33 deletions Sources/_OpenAPIGeneratorCore/Hooks/FilteredDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ struct FilteredDocumentBuilder {
guard let methods = requiredEndpoints[path] else { continue }
switch pathItem {
case .a(let reference):
components.pathItems[try reference.internalComponentKey] = try document.components.lookup(reference)
components.pathItems[try reference.internalComponentKey] = try document.components.assumeLookupOnce(reference)
.filteringEndpoints { methods.contains($0.method) }
case .b(let pathItem):
filteredDocument.paths[path] = .b(pathItem.filteringEndpoints { methods.contains($0.method) })
Expand Down Expand Up @@ -169,7 +169,7 @@ struct FilteredDocumentBuilder {
/// - Parameter name: The key in the `#/components/schemas` map in the OpenAPI document.
/// - Throws: If the named schema does not exist in original OpenAPI document.
mutating func includeSchema(_ name: String) throws {
try includeSchema(.a(OpenAPI.Reference<JSONSchema>.component(named: name)))
try includeComponentsReferencedBy(.reference(.component(named: name)))
}
}

Expand Down Expand Up @@ -198,16 +198,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredPathItemReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}

mutating func includeSchema(_ maybeReference: Either<OpenAPI.Reference<JSONSchema>, JSONSchema>) throws {
switch maybeReference {
case .a(let reference):
guard try requiredSchemaReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -218,7 +209,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredParameterReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -229,7 +220,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredResponseReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -238,7 +229,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredHeaderReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -247,7 +238,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredLinkReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -258,7 +249,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredCallbacksReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -269,7 +260,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredRequestReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -278,7 +269,7 @@ private extension FilteredDocumentBuilder {
switch maybeReference {
case .a(let reference):
guard try requiredExampleReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let value): try includeComponentsReferencedBy(value)
}
}
Expand All @@ -287,7 +278,7 @@ private extension FilteredDocumentBuilder {
for (path, maybePathItemReference) in document.paths {
let originalPathItem: OpenAPI.PathItem
switch maybePathItemReference {
case .a(let reference): originalPathItem = try document.components.lookup(reference)
case .a(let reference): originalPathItem = try document.components.assumeLookupOnce(reference)
case .b(let pathItem): originalPathItem = pathItem
}

Expand Down Expand Up @@ -330,7 +321,7 @@ private extension FilteredDocumentBuilder {
case .reference(let reference, _):
let referenceKey = try OpenAPI.ComponentKey(stringLiteral: reference.requiredName)
guard requiredSchemaReferences.insert(referenceKey).inserted else { return }
try includeComponentsReferencedBy(document.components.lookup(reference))
try includeComponentsReferencedBy(document.components.lookupOnce(reference).flattenToJsonSchema())

case .object(_, let object):
for schema in object.properties.values { try includeComponentsReferencedBy(schema) }
Expand Down Expand Up @@ -362,20 +353,23 @@ private extension FilteredDocumentBuilder {
switch schemaContext.schema {
case .a(let reference):
guard try requiredSchemaReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
try includeComponentsReferencedBy(try document.components.assumeLookupOnce(reference))
case .b(let schema): try includeComponentsReferencedBy(schema)
}
case .b(let contentMap):
for value in contentMap.values {
switch value.schema {
case .a(let reference):
guard try requiredSchemaReferences.insert(reference.internalComponentKey).inserted else { return }
try includeComponentsReferencedBy(try document.components.lookup(reference))
case .b(let schema): try includeComponentsReferencedBy(schema)
case .none: continue
}
}
for value in contentMap.values { try includeComponentsReferencedBy(value) }
}
}

mutating func includeComponentsReferencedBy(
_ contentMapEntry: Either<OpenAPI.Reference<OpenAPI.Content>, OpenAPI.Content>
) throws {
let content: OpenAPI.Content
switch contentMapEntry {
case .a(let ref): content = try document.components.assumeLookupOnce(ref)
case .b(let value): content = value
}
try includeComponentsReferencedBy(content)
}

mutating func includeComponentsReferencedBy(_ response: OpenAPI.Response) throws {
Expand All @@ -385,8 +379,8 @@ private extension FilteredDocumentBuilder {
}

mutating func includeComponentsReferencedBy(_ content: OpenAPI.Content) throws {
if let schema = content.schema { try includeSchema(schema) }
if let encoding = content.encoding {
if let schema = content.schema { try includeComponentsReferencedBy(schema) }
if let encoding = content.encodingMap {
for encoding in encoding.values {
if let headers = encoding.headers { for header in headers.values { try includeHeader(header) } }
}
Expand Down
13 changes: 1 addition & 12 deletions Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,6 @@ public struct YamsParser: ParserProtocol {
let decoder = YAMLDecoder()
let openapiData = input.contents

let decodingOptions = [
DocumentConfiguration.versionMapKey: [
// Until we move to OpenAPIKit v5.0+ we will parse OAS 3.2.0 as if it were OAS 3.1.2
"3.2.0": OpenAPI.Document.Version.v3_1_2
]
]

struct OpenAPIVersionedDocument: Decodable { var openapi: String? }

let versionedDocument: OpenAPIVersionedDocument
Expand All @@ -83,11 +76,7 @@ public struct YamsParser: ParserProtocol {
case "3.1.0", "3.1.1", "3.1.2":
document = try decoder.decode(OpenAPIKit.OpenAPI.Document.self, from: input.contents)
case "3.2.0":
document = try decoder.decode(
OpenAPIKit.OpenAPI.Document.self,
from: input.contents,
userInfo: decodingOptions
)
document = try decoder.decode(OpenAPIKit.OpenAPI.Document.self, from: input.contents)
default:
throw Diagnostic.openAPIVersionError(
versionString: "openapi: \(openAPIVersion)",
Expand Down
35 changes: 23 additions & 12 deletions Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String
}

for (key, component) in doc.components.requestBodies {
let component = try doc.components.assumeLookupOnce(component)
for contentType in component.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
Expand All @@ -81,6 +82,7 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String
}

for (key, component) in doc.components.responses {
let component = try doc.components.assumeLookupOnce(component)
for contentType in component.content.keys {
if !validate(contentType.rawValue) {
throw Diagnostic.error(
Expand Down Expand Up @@ -124,21 +126,30 @@ func validateReferences(in doc: ParsedOpenAPIRepresentation) throws {

func validateReferencesInContentTypes(_ content: OpenAPI.Content.Map, location: String) throws {
for (contentKey, contentType) in content {
if let reference = contentType.schema?.reference {
switch contentType {
case .a(let ref):
try validateReference(
reference,
ref,
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/schema"
location: location + "/content/\(contentKey.rawValue)"
)
}
if let eitherExamples = contentType.examples?.values {
for example in eitherExamples {
if let reference = example.reference {
try validateReference(
reference,
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/examples"
)
case .b(let contentType):
if let reference: JSONReference<JSONSchema> = contentType.schema?.reference {
try validateReference(
.init(reference),
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/schema"
)
}
if let eitherExamples = contentType.examples?.values {
for example in eitherExamples {
if let reference = example.reference {
try validateReference(
reference,
in: doc.components,
location: location + "/content/\(contentKey.rawValue)/examples"
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ extension FileTranslator {
// In nullable enum schemas, empty strings are parsed as Void.
// This is unlikely to be fixed, so handling that case here.
// https://github.com/apple/swift-openapi-generator/issues/118
if isNullable && anyValue is Void {
// Also handle nil values in nullable schemas.
let isNullValue = anyValue is Void || (anyValue as? String) == nil
if isNullable && isNullValue {
Comment on lines +103 to +105
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!

try addIfUnique(id: .string(""), caseName: context.safeNameGenerator.swiftMemberName(for: ""))
} else {
guard let rawValue = anyValue as? String else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ extension FileTranslator {
return try contents.compactMap { key, value in
try parseContentIfSupported(
contentKey: key,
contentValue: value,
contentValue: try components.assumeLookupOnce(value),
excludeBinary: excludeBinary,
isRequired: isRequired,
foundIn: foundIn + "/\(key.rawValue)"
Expand Down Expand Up @@ -129,12 +129,14 @@ extension FileTranslator {

let chosenContent: (type: ContentType, schema: SchemaContent, content: OpenAPI.Content)?
if let (contentType, contentValue) = mapWithContentTypes.first(where: { $0.type.isJSON }) {
chosenContent = (contentType, .init(contentType: contentType, schema: contentValue.schema), contentValue)
let contentValue = try components.assumeLookupOnce(contentValue)
chosenContent = (contentType, .init(contentType: contentType, schema: contentValue.schema.map(Either.schema)), contentValue)
} else if !excludeBinary,
let (contentType, contentValue) = mapWithContentTypes.first(where: { $0.type.isBinary })
{
let contentValue = try components.assumeLookupOnce(contentValue)
chosenContent = (
contentType, .init(contentType: contentType, schema: .b(.string(contentEncoding: .binary))),
contentType, .init(contentType: contentType, schema: .schema(.string(contentEncoding: .binary))),
contentValue
)
} else {
Expand Down Expand Up @@ -188,8 +190,8 @@ extension FileTranslator {
foundIn: "\(foundIn), content \(contentType.originallyCasedTypeAndSubtype)"
)
}
if contentType.isJSON { return .init(contentType: contentType, schema: contentValue.schema) }
if contentType.isUrlEncodedForm { return .init(contentType: contentType, schema: contentValue.schema) }
if contentType.isJSON { return .init(contentType: contentType, schema: contentValue.schema.map(Either.schema)) }
if contentType.isUrlEncodedForm { return .init(contentType: contentType, schema: contentValue.schema.map(Either.schema)) }
if contentType.isMultipart {
guard isRequired else {
try diagnostics.emit(
Expand All @@ -201,10 +203,10 @@ extension FileTranslator {
)
return nil
}
return .init(contentType: contentType, schema: contentValue.schema, encoding: contentValue.encoding)
return .init(contentType: contentType, schema: contentValue.schema.map(Either.schema), encoding: contentValue.encodingMap)
}
if !excludeBinary, contentType.isBinary {
return .init(contentType: contentType, schema: .b(.string(contentEncoding: .binary)))
return .init(contentType: contentType, schema: .schema(.string(contentEncoding: .binary)))
}
try diagnostics.emitUnsupported("Unsupported content", foundIn: foundIn)
return nil
Expand Down
Loading
Loading