Skip to content

Commit 5dbd05d

Browse files
authored
Add support for nullable schemas (#248)
### Motivation Fixes #82. More background: In JSON Schema, there are two fields that affect what we represent as optionality in Swift. 1. In object schemas, not including a property in the `required` array. 2. In all schemas, marking the schema as nullable. In OpenAPI 3.0, that used to be done using a dedicated `nullable` field, in OpenAPI 3.1, that field is removed and instead the same result is achieved by adding the `null` type in the `type` array. So for a non-nullable string, you'd use `type: string`, and for a nullable string, you'd use `type: [string, null]`. Up until now, we've only supported (1) in how we decide which types are generated as optional, and that seems to be how most adopters control nullability. However, (2) is also supported in JSON Schema, so we should also support it, and as evidenced by the comments in #82, a few folks have hit the lack of nullability support in Swift OpenAPI Generator. ### Modifications This PR takes nullability on the schema into account when deciding whether to generate a type as optional or not. Since this is a source-breaking change, it's introduced behind the feature flag `nullableSchemas`, and it'll become default behavior in 0.3.0. Most of this change is just propagating the feature flag to the right place. ### Result Marking a schema as nullable will result in an optional type being generated. ### Test Plan Added a snippet test, both with and without the feature flag, which shows the change.
1 parent 99e3102 commit 5dbd05d

File tree

6 files changed

+134
-10
lines changed

6 files changed

+134
-10
lines changed

Sources/_OpenAPIGeneratorCore/FeatureFlags.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
/// 1.0 is released.)
2828
public enum FeatureFlag: String, Hashable, Codable, CaseIterable {
2929

30-
/// Has to be here until we add more feature flags, otherwise the enum
31-
/// doesn't compile.
32-
case empty
30+
/// Support for `nullable` schemas.
31+
///
32+
/// A dedicated field in OpenAPI 3.0, a `null` value present in
33+
/// the `types` array in OpenAPI 3.1.
34+
case nullableSchemas
3335
}
3436

3537
/// A set of enabled feature flags.

Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@ import OpenAPIKit
1515

1616
extension FileTranslator {
1717
// Add helpers for reading feature flags below.
18+
19+
/// A Boolean value indicating whether the `nullable` field on schemas
20+
/// should be taken into account.
21+
var supportNullableSchemas: Bool {
22+
config.featureFlags.contains(.nullableSchemas)
23+
}
1824
}

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ struct TypeAssigner {
4545
/// safe to be used as a Swift identifier.
4646
var asSwiftSafeName: (String) -> String
4747

48+
/// A Boolean value indicating whether the `nullable` field on schemas
49+
/// should be taken into account.
50+
var supportNullableSchemas: Bool
51+
4852
/// Returns a type name for an OpenAPI-named component type.
4953
///
5054
/// A component type is any type in `#/components` in the OpenAPI document.
@@ -256,7 +260,10 @@ struct TypeAssigner {
256260
// Check if this type can be simply referenced without
257261
// creating a new inline type.
258262
if let referenceableType =
259-
try TypeMatcher(asSwiftSafeName: asSwiftSafeName)
263+
try TypeMatcher(
264+
asSwiftSafeName: asSwiftSafeName,
265+
supportNullableSchemas: supportNullableSchemas
266+
)
260267
.tryMatchReferenceableType(for: schema)
261268
{
262269
return referenceableType
@@ -452,12 +459,18 @@ extension FileTranslator {
452459

453460
/// A configured type assigner.
454461
var typeAssigner: TypeAssigner {
455-
TypeAssigner(asSwiftSafeName: swiftSafeName)
462+
TypeAssigner(
463+
asSwiftSafeName: swiftSafeName,
464+
supportNullableSchemas: supportNullableSchemas
465+
)
456466
}
457467

458468
/// A configured type matcher.
459469
var typeMatcher: TypeMatcher {
460-
TypeMatcher(asSwiftSafeName: swiftSafeName)
470+
TypeMatcher(
471+
asSwiftSafeName: swiftSafeName,
472+
supportNullableSchemas: supportNullableSchemas
473+
)
461474
}
462475
}
463476

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ struct TypeMatcher {
2020
/// safe to be used as a Swift identifier.
2121
var asSwiftSafeName: (String) -> String
2222

23+
/// A Boolean value indicating whether the `nullable` field on schemas
24+
/// should be taken into account.
25+
var supportNullableSchemas: Bool
26+
2327
/// Returns the type name of a built-in type that matches the specified
2428
/// schema.
2529
///
@@ -78,8 +82,11 @@ struct TypeMatcher {
7882
guard case let .reference(ref, _) = schema else {
7983
return nil
8084
}
81-
return try TypeAssigner(asSwiftSafeName: asSwiftSafeName)
82-
.typeName(for: ref).asUsage
85+
return try TypeAssigner(
86+
asSwiftSafeName: asSwiftSafeName,
87+
supportNullableSchemas: supportNullableSchemas
88+
)
89+
.typeName(for: ref).asUsage
8390
},
8491
matchedArrayHandler: { elementType in
8592
elementType.asArray
@@ -88,7 +95,7 @@ struct TypeMatcher {
8895
TypeName.arrayContainer.asUsage
8996
}
9097
)?
91-
.withOptional(!schema.required)
98+
.withOptional(!schema.required || (supportNullableSchemas && schema.nullable))
9299
}
93100

94101
/// Returns a Boolean value that indicates whether the schema

Sources/swift-openapi-generator/Documentation.docc/Articles/Supported-OpenAPI-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Supported features are always provided on _both_ client and server.
155155
- [x] description
156156
- [x] format
157157
- [ ] default
158-
- [ ] nullable (only in 3.0, removed in 3.1)
158+
- [x] nullable (only in 3.0, removed in 3.1, add `null` in `types` instead)
159159
- [x] discriminator
160160
- [ ] readOnly
161161
- [ ] writeOnly

Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,102 @@ final class SnippetBasedReferenceTests: XCTestCase {
101101
)
102102
}
103103

104+
func testComponentsSchemasNullableStringProperty() throws {
105+
try self.assertSchemasTranslation(
106+
"""
107+
schemas:
108+
MyObj:
109+
type: object
110+
properties:
111+
fooOptional:
112+
type: string
113+
fooRequired:
114+
type: string
115+
fooOptionalNullable:
116+
type: [string, null]
117+
fooRequiredNullable:
118+
type: [string, null]
119+
required:
120+
- fooRequired
121+
- fooRequiredNullable
122+
""",
123+
"""
124+
public enum Schemas {
125+
public struct MyObj: Codable, Hashable, Sendable {
126+
public var fooOptional: Swift.String?
127+
public var fooRequired: Swift.String
128+
public var fooOptionalNullable: Swift.String?
129+
public var fooRequiredNullable: Swift.String
130+
public init(
131+
fooOptional: Swift.String? = nil,
132+
fooRequired: Swift.String,
133+
fooOptionalNullable: Swift.String? = nil,
134+
fooRequiredNullable: Swift.String
135+
) {
136+
self.fooOptional = fooOptional
137+
self.fooRequired = fooRequired
138+
self.fooOptionalNullable = fooOptionalNullable
139+
self.fooRequiredNullable = fooRequiredNullable
140+
}
141+
public enum CodingKeys: String, CodingKey {
142+
case fooOptional
143+
case fooRequired
144+
case fooOptionalNullable
145+
case fooRequiredNullable
146+
}
147+
}
148+
}
149+
"""
150+
)
151+
try self.assertSchemasTranslation(
152+
featureFlags: [.nullableSchemas],
153+
"""
154+
schemas:
155+
MyObj:
156+
type: object
157+
properties:
158+
fooOptional:
159+
type: string
160+
fooRequired:
161+
type: string
162+
fooOptionalNullable:
163+
type: [string, null]
164+
fooRequiredNullable:
165+
type: [string, null]
166+
required:
167+
- fooRequired
168+
- fooRequiredNullable
169+
""",
170+
"""
171+
public enum Schemas {
172+
public struct MyObj: Codable, Hashable, Sendable {
173+
public var fooOptional: Swift.String?
174+
public var fooRequired: Swift.String
175+
public var fooOptionalNullable: Swift.String?
176+
public var fooRequiredNullable: Swift.String?
177+
public init(
178+
fooOptional: Swift.String? = nil,
179+
fooRequired: Swift.String,
180+
fooOptionalNullable: Swift.String? = nil,
181+
fooRequiredNullable: Swift.String? = nil
182+
) {
183+
self.fooOptional = fooOptional
184+
self.fooRequired = fooRequired
185+
self.fooOptionalNullable = fooOptionalNullable
186+
self.fooRequiredNullable = fooRequiredNullable
187+
}
188+
public enum CodingKeys: String, CodingKey {
189+
case fooOptional
190+
case fooRequired
191+
case fooOptionalNullable
192+
case fooRequiredNullable
193+
}
194+
}
195+
}
196+
"""
197+
)
198+
}
199+
104200
func testComponentsObjectNoAdditionalProperties() throws {
105201
try self.assertSchemasTranslation(
106202
"""

0 commit comments

Comments
 (0)