diff --git a/Sources/_OpenAPIGeneratorCore/Extensions/OpenAPIKit.swift b/Sources/_OpenAPIGeneratorCore/Extensions/OpenAPIKit.swift index 791c11c1..b52e50b1 100644 --- a/Sources/_OpenAPIGeneratorCore/Extensions/OpenAPIKit.swift +++ b/Sources/_OpenAPIGeneratorCore/Extensions/OpenAPIKit.swift @@ -109,4 +109,10 @@ extension JSONSchema { /// Returns a human-readable description of the schema. var prettyDescription: String { value.prettyDescription } + + /// A Boolean value indicating whether the schema is the `null` type. + var isNullType: Bool { + if case .null = value { return true } + return false + } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift index 1816cabc..43453a4d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift @@ -40,6 +40,8 @@ extension TypesFileTranslator { func translateAllOrAnyOf(typeName: TypeName, openAPIDescription: String?, type: AllOrAnyOf, schemas: [JSONSchema]) throws -> Declaration { + // A `null` branch only marks the composition as nullable, it has no member. + let schemas = schemas.filter { !$0.isNullType } let properties: [(property: PropertyBlueprint, isKeyValuePair: Bool)] = try schemas.enumerated() .map { index, schema in let key = "value\(index+1)" @@ -129,6 +131,8 @@ extension TypesFileTranslator { discriminator: OpenAPI.Discriminator?, schemas: [JSONSchema] ) throws -> Declaration { + // A `null` branch only marks the oneOf as nullable, it has no case. + let schemas = schemas.filter { !$0.isNullType } let cases: [(String, [String]?, Bool, Comment?, TypeUsage, [Declaration])] if let discriminator { // > When using the discriminator, inline schemas will not be considered. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateArray.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateArray.swift index c9958420..37cd6e2f 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateArray.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateArray.swift @@ -58,7 +58,8 @@ extension TypesFileTranslator { .`typealias`( accessModifier: config.access, name: typeName.shortSwiftName, - existingType: .init(elementType.typeName.asUsage.asArray) + // Preserve the element's optionality, like builtin `[Element?]`. + existingType: .init(elementType.asArray) ) ) return inline + [arrayDecl] diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift index 1a415312..785f1996 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift @@ -157,12 +157,16 @@ extension FileTranslator { // an array is supported iff its element schema is supported return try isSchemaSupported(items, referenceStack: &referenceStack) case .all(of: let schemas, _): + // A `null` branch only marks the composition as nullable, ignore it here. + let schemas = schemas.filter { !$0.isNullType } guard !schemas.isEmpty else { return .unsupported(reason: .noSubschemas, schema: schema) } return try areSchemasSupported(schemas, referenceStack: &referenceStack) case .any(of: let schemas, _): + let schemas = schemas.filter { !$0.isNullType } guard !schemas.isEmpty else { return .unsupported(reason: .noSubschemas, schema: schema) } return try areSchemasSupported(schemas, referenceStack: &referenceStack) case .one(of: let schemas, let context): + let schemas = schemas.filter { !$0.isNullType } guard !schemas.isEmpty else { return .unsupported(reason: .noSubschemas, schema: schema) } guard context.discriminator != nil else { return try areSchemasSupported(schemas, referenceStack: &referenceStack) diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 6ea8d60b..c3afe790 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1099,6 +1099,304 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasOneOfWithNull_singleRef() throws { + try self.assertSchemasTranslation( + """ + schemas: + B: + type: object + required: [c] + properties: + c: + type: string + A: + type: object + required: [b] + properties: + b: + oneOf: + - $ref: '#/components/schemas/B' + - type: 'null' + """, + """ + public enum Schemas { + public struct B: Codable, Hashable, Sendable { + public var c: Swift.String + public init(c: Swift.String) { + self.c = c + } + public enum CodingKeys: String, CodingKey { + case c + } + } + public struct A: Codable, Hashable, Sendable { + @frozen public enum bPayload: Codable, Hashable, Sendable { + case B(Components.Schemas.B) + public init(from decoder: any Swift.Decoder) throws { + var errors: [any Swift.Error] = [] + do { + self = .B(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + throw Swift.DecodingError.failedToDecodeOneOfSchema( + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + public func encode(to encoder: any Swift.Encoder) throws { + switch self { + case let .B(value): + try value.encode(to: encoder) + } + } + } + public var b: Components.Schemas.A.bPayload? + public init(b: Components.Schemas.A.bPayload? = nil) { + self.b = b + } + public enum CodingKeys: String, CodingKey { + case b + } + } + } + """ + ) + } + + func testComponentsSchemasOneOfWithNull_multiRef() throws { + try self.assertSchemasTranslation( + """ + schemas: + B1: + type: object + required: [c] + properties: + c: + type: string + B2: + type: object + required: [d] + properties: + d: + type: string + A: + type: object + required: [b] + properties: + b: + oneOf: + - $ref: '#/components/schemas/B1' + - $ref: '#/components/schemas/B2' + - type: 'null' + """, + """ + public enum Schemas { + public struct B1: Codable, Hashable, Sendable { + public var c: Swift.String + public init(c: Swift.String) { + self.c = c + } + public enum CodingKeys: String, CodingKey { + case c + } + } + public struct B2: Codable, Hashable, Sendable { + public var d: Swift.String + public init(d: Swift.String) { + self.d = d + } + public enum CodingKeys: String, CodingKey { + case d + } + } + public struct A: Codable, Hashable, Sendable { + @frozen public enum bPayload: Codable, Hashable, Sendable { + case B1(Components.Schemas.B1) + case B2(Components.Schemas.B2) + public init(from decoder: any Swift.Decoder) throws { + var errors: [any Swift.Error] = [] + do { + self = .B1(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + do { + self = .B2(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + throw Swift.DecodingError.failedToDecodeOneOfSchema( + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + public func encode(to encoder: any Swift.Encoder) throws { + switch self { + case let .B1(value): + try value.encode(to: encoder) + case let .B2(value): + try value.encode(to: encoder) + } + } + } + public var b: Components.Schemas.A.bPayload? + public init(b: Components.Schemas.A.bPayload? = nil) { + self.b = b + } + public enum CodingKeys: String, CodingKey { + case b + } + } + } + """ + ) + } + + func testComponentsSchemasAnyOfWithNull_singleRef() throws { + try self.assertSchemasTranslation( + """ + schemas: + B: + type: object + required: [c] + properties: + c: + type: string + A: + type: object + required: [b] + properties: + b: + anyOf: + - $ref: '#/components/schemas/B' + - type: 'null' + """, + """ + public enum Schemas { + public struct B: Codable, Hashable, Sendable { + public var c: Swift.String + public init(c: Swift.String) { + self.c = c + } + public enum CodingKeys: String, CodingKey { + case c + } + } + public struct A: Codable, Hashable, Sendable { + public struct bPayload: Codable, Hashable, Sendable { + public var value1: Components.Schemas.B? + public init(value1: Components.Schemas.B? = nil) { + self.value1 = value1 + } + public init(from decoder: any Swift.Decoder) throws { + var errors: [any Swift.Error] = [] + do { + self.value1 = try .init(from: decoder) + } catch { + errors.append(error) + } + try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( + [ + self.value1 + ], + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + public func encode(to encoder: any Swift.Encoder) throws { + try self.value1?.encode(to: encoder) + } + } + public var b: Components.Schemas.A.bPayload? + public init(b: Components.Schemas.A.bPayload? = nil) { + self.b = b + } + public enum CodingKeys: String, CodingKey { + case b + } + } + } + """ + ) + } + + func testComponentsSchemasArrayWithNullableOneOfItems() throws { + try self.assertSchemasTranslation( + """ + schemas: + B: + type: object + required: [c] + properties: + c: + type: string + A: + type: object + required: [items] + properties: + items: + type: array + items: + oneOf: + - $ref: '#/components/schemas/B' + - type: 'null' + """, + """ + public enum Schemas { + public struct B: Codable, Hashable, Sendable { + public var c: Swift.String + public init(c: Swift.String) { + self.c = c + } + public enum CodingKeys: String, CodingKey { + case c + } + } + public struct A: Codable, Hashable, Sendable { + @frozen public enum itemsPayloadPayload: Codable, Hashable, Sendable { + case B(Components.Schemas.B) + public init(from decoder: any Swift.Decoder) throws { + var errors: [any Swift.Error] = [] + do { + self = .B(try .init(from: decoder)) + return + } catch { + errors.append(error) + } + throw Swift.DecodingError.failedToDecodeOneOfSchema( + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + public func encode(to encoder: any Swift.Encoder) throws { + switch self { + case let .B(value): + try value.encode(to: encoder) + } + } + } + public typealias itemsPayload = [Components.Schemas.A.itemsPayloadPayload?] + public var items: Components.Schemas.A.itemsPayload + public init(items: Components.Schemas.A.itemsPayload) { + self.items = items + } + public enum CodingKeys: String, CodingKey { + case items + } + } + } + """ + ) + } + func testComponentsSchemasOneOf_open_pattern() throws { try self.assertSchemasTranslation( """