Skip to content

Commit 22830f2

Browse files
authored
Propagate nullability through references (#300)
Propagate nullability through references ### Motivation Fixes #288. ### Modifications Refactors how we decide whether to mark a type as optional, and adds the ability to "peek" through references to decide the nullability of the reference schema. Also added an article describing the overall logic as a starting point, will be expanded with a few more changes coming in this area in future PRs. ### Result We now correctly mark an object property as optional if it points at an optional schema through a reference. Previously, it was always marked as required, which caused decoding to fail e.g. in the GitHub OpenAPI doc. ### Test Plan Added and adjusted unit tests. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (compatibility test) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. ✖︎ pull request validation (integration test) - Build finished. #300
1 parent b4ad524 commit 22830f2

File tree

14 files changed

+289
-16
lines changed

14 files changed

+289
-16
lines changed

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ extension FileTranslator {
4949
let rawPropertyType = try typeAssigner.typeUsage(
5050
forAllOrAnyOrOneOfChildSchemaNamed: key,
5151
withSchema: schema,
52+
components: components,
5253
inParent: typeName
5354
)
5455
let propertyType: TypeUsage
@@ -163,6 +164,7 @@ extension FileTranslator {
163164
let childType = try typeAssigner.typeUsage(
164165
forAllOrAnyOrOneOfChildSchemaNamed: key,
165166
withSchema: schema,
167+
components: components,
166168
inParent: typeName
167169
)
168170
let caseName: String

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateArray.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ extension FileTranslator {
3737
// An OpenAPI array is represented as a Swift array with an element type
3838
let elementType: TypeUsage
3939
if let items = arrayContext.items {
40-
if let builtinType = try typeMatcher.tryMatchReferenceableType(for: items) {
40+
if let builtinType = try typeMatcher.tryMatchReferenceableType(
41+
for: items,
42+
components: components
43+
) {
4144
elementType = builtinType
4245
} else {
4346
elementType = try typeAssigner.typeUsage(
4447
forArrayElementWithSchema: items,
48+
components: components,
4549
inParent: typeName
4650
)
4751
let nestedDecls = try translateSchema(

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ extension FileTranslator {
4343
let propertyType = try typeAssigner.typeUsage(
4444
forObjectPropertyNamed: key,
4545
withSchema: value,
46+
components: components,
4647
inParent: typeName
4748
)
4849
let comment: Comment? = .property(
@@ -127,6 +128,7 @@ extension FileTranslator {
127128
let valueTypeUsage = try typeAssigner.typeUsage(
128129
forObjectPropertyNamed: "additionalProperties",
129130
withSchema: schema,
131+
components: components,
130132
inParent: parent
131133
)
132134
if TypeMatcher.isInlinable(schema) {

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,16 @@ extension FileTranslator {
8989
}
9090

9191
// If this type maps to a referenceable schema, define a typealias
92-
if let builtinType = try typeMatcher.tryMatchReferenceableType(for: schema) {
92+
if let builtinType = try typeMatcher.tryMatchReferenceableType(
93+
for: schema,
94+
components: components
95+
) {
9396
let typealiasDecl = try translateTypealias(
9497
named: typeName,
9598
userDescription: overrides.userDescription ?? schema.description,
96-
to: builtinType.withOptional(overrides.isOptional ?? !schema.required)
99+
to: builtinType.withOptional(
100+
overrides.isOptional ?? typeMatcher.isOptional(schema, components: components)
101+
)
97102
)
98103
return [typealiasDecl]
99104
}

Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ extension FileTranslator {
5656
let associatedType = try typeAssigner.typeUsage(
5757
usingNamingHint: identifier,
5858
withSchema: content.schema,
59+
components: components,
5960
inParent: parent
6061
)
6162
return .init(content: content, typeUsage: associatedType)
@@ -91,6 +92,7 @@ extension FileTranslator {
9192
let associatedType = try typeAssigner.typeUsage(
9293
usingNamingHint: identifier,
9394
withSchema: content.schema,
95+
components: components,
9496
inParent: parent
9597
)
9698
return .init(content: content, typeUsage: associatedType)

Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ extension FileTranslator {
238238
type = try typeAssigner.typeUsage(
239239
forParameterNamed: _parameter.name,
240240
withSchema: schema,
241+
components: components,
241242
inParent: locationTypeName
242243
)
243244
}

Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ extension FileTranslator {
154154
type = try typeAssigner.typeUsage(
155155
forParameterNamed: name,
156156
withSchema: schema,
157+
components: components,
157158
inParent: parent
158159
)
159160
}

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,13 @@ struct TypeAssigner {
8383
/// - Parameters:
8484
/// - hint: A hint string used when computing a name for an inline type.
8585
/// - schema: The OpenAPI schema.
86+
/// - components: The components in which to look up references.
8687
/// - parent: The parent type in which to name the type.
8788
/// - Returns: A type usage; or nil if the schema is nil or unsupported.
8889
func typeUsage(
8990
usingNamingHint hint: String,
9091
withSchema schema: UnresolvedSchema?,
92+
components: OpenAPI.Components,
9193
inParent parent: TypeName
9294
) throws -> TypeUsage? {
9395
let associatedType: TypeUsage?
@@ -99,6 +101,7 @@ struct TypeAssigner {
99101
associatedType = try _typeUsage(
100102
forPotentiallyInlinedValueNamed: hint,
101103
withSchema: schema,
104+
components: components,
102105
inParent: parent,
103106
subtype: .appendScope
104107
)
@@ -113,16 +116,19 @@ struct TypeAssigner {
113116
/// - Parameters:
114117
/// - originalName: The name of the property in the OpenAPI document.
115118
/// - schema: The OpenAPI schema provided for the property.
119+
/// - components: The components in which to look up references.
116120
/// - parent: The parent type in which to name the type.
117121
/// - Returns: A type usage.
118122
func typeUsage(
119123
forObjectPropertyNamed originalName: String,
120124
withSchema schema: JSONSchema,
125+
components: OpenAPI.Components,
121126
inParent parent: TypeName
122127
) throws -> TypeUsage {
123128
try _typeUsage(
124129
forPotentiallyInlinedValueNamed: originalName,
125130
withSchema: schema,
131+
components: components,
126132
inParent: parent,
127133
subtype: .appendScope
128134
)
@@ -132,17 +138,20 @@ struct TypeAssigner {
132138
/// - Parameters:
133139
/// - originalName: A hint for naming.
134140
/// - schema: The OpenAPI schema provided for the property.
141+
/// - components: The components in which to look up references.
135142
/// - parent: The parent type in which to name the type.
136143
/// - Returns: A type usage.
137144
func typeUsage(
138145
forAllOrAnyOrOneOfChildSchemaNamed originalName: String,
139146
withSchema schema: JSONSchema,
147+
components: OpenAPI.Components,
140148
inParent parent: TypeName
141149
) throws -> TypeUsage {
142150
try _typeUsage(
143151
forPotentiallyInlinedValueNamed: originalName.uppercasingFirstLetter,
144152
jsonReferenceComponentOverride: originalName,
145153
withSchema: schema,
154+
components: components,
146155
inParent: parent,
147156
subtype: .appendScope
148157
)
@@ -151,15 +160,18 @@ struct TypeAssigner {
151160
/// Returns a type usage for an element schema of an array.
152161
/// - Parameters:
153162
/// - schema: The OpenAPI schema provided for the array element type.
163+
/// - components: The components in which to look up references.
154164
/// - parent: The parent type in which to name the type.
155165
/// - Returns: A type usage.
156166
func typeUsage(
157167
forArrayElementWithSchema schema: JSONSchema,
168+
components: OpenAPI.Components,
158169
inParent parent: TypeName
159170
) throws -> TypeUsage {
160171
try _typeUsage(
161172
forPotentiallyInlinedValueNamed: parent.shortSwiftName,
162173
withSchema: schema,
174+
components: components,
163175
inParent: parent,
164176
subtype: .appendToLastPathComponent
165177
)
@@ -169,16 +181,19 @@ struct TypeAssigner {
169181
/// - Parameters:
170182
/// - originalName: The name of the parameter in the OpenAPI document.
171183
/// - schema: The OpenAPI schema provided for the parameter.
184+
/// - components: The components in which to look up references.
172185
/// - parent: The parent type in which to name the type.
173186
/// - Returns: A type usage.
174187
func typeUsage(
175188
forParameterNamed originalName: String,
176189
withSchema schema: JSONSchema,
190+
components: OpenAPI.Components,
177191
inParent parent: TypeName
178192
) throws -> TypeUsage {
179193
try _typeUsage(
180194
forPotentiallyInlinedValueNamed: originalName,
181195
withSchema: schema,
196+
components: components,
182197
inParent: parent,
183198
subtype: .appendScope
184199
)
@@ -250,17 +265,19 @@ struct TypeAssigner {
250265
jsonReferenceComponentOverride: String? = nil,
251266
suffix: String = Constants.Global.inlineTypeSuffix,
252267
withSchema schema: JSONSchema,
268+
components: OpenAPI.Components,
253269
inParent parent: TypeName,
254270
subtype: SubtypeNamingMethod
255271
) throws -> TypeUsage {
272+
let typeMatcher = TypeMatcher(
273+
asSwiftSafeName: asSwiftSafeName
274+
)
256275
// Check if this type can be simply referenced without
257276
// creating a new inline type.
258-
if let referenceableType =
259-
try TypeMatcher(
260-
asSwiftSafeName: asSwiftSafeName
261-
)
262-
.tryMatchReferenceableType(for: schema)
263-
{
277+
if let referenceableType = try typeMatcher.tryMatchReferenceableType(
278+
for: schema,
279+
components: components
280+
) {
264281
return referenceableType
265282
}
266283
// Otherwise it's an inline, non-referenceable type
@@ -277,7 +294,7 @@ struct TypeAssigner {
277294
jsonComponent: jsonReferenceComponentOverride ?? originalName
278295
)
279296
.asUsage
280-
.withOptional(!schema.required)
297+
.withOptional(try typeMatcher.isOptional(schema, components: components))
281298
}
282299

283300
/// Returns a type name for a reusable component.

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@ struct TypeMatcher {
6363
/// - A reference
6464
///
6565
/// - Note: Optionality from the `JSONSchema` is applied.
66-
/// - Parameter schema: The schema to match a referenceable type for.
66+
/// - Parameters:
67+
/// - schema: The schema to match a referenceable type for.
68+
/// - components: The components in which to look up references.
6769
/// - Returns: A type usage for the schema if the schema is supported.
6870
/// Otherwise, returns nil.
6971
func tryMatchReferenceableType(
70-
for schema: JSONSchema
72+
for schema: JSONSchema,
73+
components: OpenAPI.Components
7174
) throws -> TypeUsage? {
7275
try Self._tryMatchRecursive(
7376
for: schema.value,
@@ -90,7 +93,7 @@ struct TypeMatcher {
9093
TypeName.arrayContainer.asUsage
9194
}
9295
)?
93-
.withOptional(!schema.required || schema.nullable)
96+
.withOptional(isOptional(schema, components: components))
9497
}
9598

9699
/// Returns a Boolean value that indicates whether the schema
@@ -240,6 +243,47 @@ struct TypeMatcher {
240243
return try isKeyValuePair(schemaToCheck, components: components)
241244
}
242245

246+
/// Returns a Boolean value indicating whether the schema is optional.
247+
/// - Parameters:
248+
/// - schema: The schema to check.
249+
/// - components: The OpenAPI components for looking up references.
250+
/// - Returns: `true` if the schema is optional, `false` otherwise.
251+
func isOptional(
252+
_ schema: JSONSchema,
253+
components: OpenAPI.Components
254+
) throws -> Bool {
255+
if schema.nullable || !schema.required {
256+
return true
257+
}
258+
guard case .reference(let ref, _) = schema.value else {
259+
return false
260+
}
261+
let targetSchema = try components.lookup(ref)
262+
return try isOptional(targetSchema, components: components)
263+
}
264+
265+
/// Returns a Boolean value indicating whether the schema is optional.
266+
/// - Parameters:
267+
/// - schema: The schema to check.
268+
/// - components: The OpenAPI components for looking up references.
269+
/// - Returns: `true` if the schema is optional, `false` otherwise.
270+
func isOptional(
271+
_ schema: UnresolvedSchema?,
272+
components: OpenAPI.Components
273+
) throws -> Bool {
274+
guard let schema else {
275+
// A nil unresolved schema represents a non-optional fragment.
276+
return false
277+
}
278+
switch schema {
279+
case .a(let ref):
280+
let targetSchema = try components.lookup(ref)
281+
return try isOptional(targetSchema, components: components)
282+
case .b(let schema):
283+
return try isOptional(schema, components: components)
284+
}
285+
}
286+
243287
// MARK: - Private
244288

245289
/// Returns the type name of a built-in type that matches the specified

Sources/swift-openapi-generator/Documentation.docc/Development/Documentation-for-maintainers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ Use the resources below if you'd like to learn more about how the generator work
1212

1313
- <doc:Converting-between-data-and-Swift-types>
1414
- <doc:Generating-custom-Codable-conformance-methods>
15+
- <doc:Handling-nullable-schemas>

0 commit comments

Comments
 (0)