diff --git a/Package.swift b/Package.swift index 7b90240a..47166359 100644 --- a/Package.swift +++ b/Package.swift @@ -52,7 +52,7 @@ let package = Package( // Read OpenAPI documents .package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.1.2"), - .package(url: "https://github.com/jpsim/Yams", "4.0.0"..<"6.0.0"), + .package(url: "https://github.com/jpsim/Yams", "5.1.0"..<"6.0.0"), // CLI Tool .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift index ad7a1b63..9b91c460 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift @@ -38,7 +38,7 @@ extension TypesFileTranslator { /// - Throws: An error if there is an issue during translation. /// - Returns: A declaration representing the translated allOf/anyOf structure. func translateAllOrAnyOf(typeName: TypeName, openAPIDescription: String?, type: AllOrAnyOf, schemas: [JSONSchema]) - throws -> Declaration + throws -> [Declaration] { let properties: [(property: PropertyBlueprint, isKeyValuePair: Bool)] = try schemas.enumerated() .map { index, schema in @@ -107,7 +107,7 @@ extension TypesFileTranslator { properties: propertyValues ) ) - return structDecl + return [structDecl] } /// Returns a declaration for a oneOf schema. @@ -128,7 +128,7 @@ extension TypesFileTranslator { openAPIDescription: String?, discriminator: OpenAPI.Discriminator?, schemas: [JSONSchema] - ) throws -> Declaration { + ) throws -> [Declaration] { let cases: [(String, [String]?, Bool, Comment?, TypeUsage, [Declaration])] if let discriminator { // > When using the discriminator, inline schemas will not be considered. @@ -148,7 +148,15 @@ extension TypesFileTranslator { return (caseName, mappedType.rawNames, true, comment, mappedType.typeName.asUsage, []) } } else { - cases = try schemas.enumerated() + let (schemas, nullSchemas) = schemas.partitioned(by: { typeMatcher.isNull($0) }) + if schemas.count == 1, nullSchemas.count > 0, let schema = schemas.first { + return try translateSchema( + typeName: typeName, + schema: schema, + overrides: .init(isOptional: true)) + } + cases = try schemas + .enumerated() .map { index, schema in let key = "case\(index+1)" let childType = try typeAssigner.typeUsage( @@ -242,6 +250,6 @@ extension TypesFileTranslator { conformances: Constants.ObjectStruct.conformances, members: caseDecls + codingKeysDecls + [decoder, encoder] ) - return .commentable(comment, enumDecl) + return [.commentable(comment, enumDecl)] } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index f7668e4f..a9f69ef4 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -141,29 +141,26 @@ extension TypesFileTranslator { arrayContext: arrayContext ) case let .all(of: schemas, core: coreContext): - let allOfDecl = try translateAllOrAnyOf( + return try translateAllOrAnyOf( typeName: typeName, openAPIDescription: overrides.userDescription ?? coreContext.description, type: .allOf, schemas: schemas ) - return [allOfDecl] case let .any(of: schemas, core: coreContext): - let anyOfDecl = try translateAllOrAnyOf( + return try translateAllOrAnyOf( typeName: typeName, openAPIDescription: overrides.userDescription ?? coreContext.description, type: .anyOf, schemas: schemas ) - return [anyOfDecl] case let .one(of: schemas, core: coreContext): - let oneOfDecl = try translateOneOf( + return try translateOneOf( typeName: typeName, openAPIDescription: overrides.userDescription ?? coreContext.description, discriminator: coreContext.discriminator, schemas: schemas ) - return [oneOfDecl] default: return [] } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift index f6c2d48a..63670507 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateTypealias.swift @@ -28,7 +28,7 @@ extension FileTranslator { let typealiasDescription = TypealiasDescription( accessModifier: config.access, name: typeName.shortSwiftName, - existingType: .init(existingTypeUsage.withOptional(false)) + existingType: .init(existingTypeUsage) ) let typealiasComment: Comment? = typeName.docCommentWithUserDescription(userDescription) return .commentable(typealiasComment, .typealias(typealiasDescription)) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index c422bec0..dfc40f98 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -70,7 +70,7 @@ extension TypesFileTranslator { let decl = try translateSchema( typeName: typeName, schema: parameter.schema, - overrides: .init(isOptional: !parameter.required, userDescription: parameter.parameter.description) + overrides: .init(userDescription: parameter.parameter.description) ) return decl } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift index 229d575a..707761d6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift @@ -114,7 +114,7 @@ extension TypesFileTranslator { let decl = try translateSchema( typeName: typeName, schema: header.schema, - overrides: .init(isOptional: header.isOptional, userDescription: header.header.description) + overrides: .init(userDescription: header.header.description) ) return decl } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift index 72e18f8a..5fc3a8ec 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift @@ -345,7 +345,7 @@ struct TypeAssigner { swiftComponent: asSwiftSafeName(originalName) + suffix, jsonComponent: jsonReferenceComponentOverride ?? originalName ) - .asUsage.withOptional(try typeMatcher.isOptional(schema, components: components)) + .asUsage.withOptional(try typeMatcher.isOptionalRoot(schema, components: components)) } /// Returns a type name for a reusable component. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 08ac4a1e..f4ef6f4b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -78,7 +78,7 @@ struct TypeMatcher { }, genericArrayHandler: { TypeName.arrayContainer.asUsage } )? - .withOptional(isOptional(schema, components: components)) + .withOptional(isOptionalRoot(schema, components: components)) } /// Returns a Boolean value that indicates whether the schema @@ -247,10 +247,29 @@ struct TypeMatcher { /// - Throws: An error if there's an issue while checking the schema. /// - Returns: `true` if the schema is optional, `false` otherwise. func isOptional(_ schema: JSONSchema, components: OpenAPI.Components) throws -> Bool { + var cache = [JSONReference: Bool]() + return try isOptional(schema, components: components, cache: &cache) + } + + /// Returns a Boolean value indicating whether the schema is optional. + /// - Parameters: + /// - schema: The schema to check. + /// - components: The OpenAPI components for looking up references. + /// - cache: Memoised optionality by reference. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ schema: JSONSchema, components: OpenAPI.Components, cache: inout [JSONReference: Bool]) throws -> Bool { if schema.nullable || !schema.required { return true } - guard case .reference(let ref, _) = schema.value else { return false } - let targetSchema = try components.lookup(ref) - return try isOptional(targetSchema, components: components) + switch schema.value { + case .null(_): + return true + case .reference(let ref, _): + return try isOptional(ref, components: components, cache: &cache) + case .one(of: let schemas, core: _): + return try schemas.contains(where: { try isOptional($0, components: components, cache: &cache) }) + default: + return schema.nullable + } } /// Returns a Boolean value indicating whether the schema is optional. @@ -260,15 +279,89 @@ struct TypeMatcher { /// - Throws: An error if there's an issue while checking the schema. /// - Returns: `true` if the schema is optional, `false` otherwise. func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components) throws -> Bool { + var cache = [JSONReference: Bool]() + return try isOptional(schema, components: components, cache: &cache) + } + + /// Returns a Boolean value indicating whether the schema is optional. + /// - Parameters: + /// - schema: The schema to check. + /// - components: The OpenAPI components for looking up references. + /// - cache: Memoised optionality by reference. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ schema: UnresolvedSchema?, components: OpenAPI.Components, cache: inout [JSONReference: Bool]) throws -> Bool { guard let schema else { // A nil unresolved schema represents a non-optional fragment. return false } switch schema { case .a(let ref): - let targetSchema = try components.lookup(ref) - return try isOptional(targetSchema, components: components) - case .b(let schema): return try isOptional(schema, components: components) + return try isOptional(ref.jsonReference, components: components, cache: &cache) + case .b(let schema): return try isOptional(schema, components: components, cache: &cache) + } + } + + /// Returns a Boolean value indicating whether the referenced schema is optional. + /// - Parameters: + /// - schema: The reference to check. + /// - components: The OpenAPI components for looking up references. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ ref: JSONReference, components: OpenAPI.Components) throws -> Bool { + var cache = [JSONReference: Bool]() + return try isOptional(ref, components: components, cache: &cache) + } + + /// Returns a Boolean value indicating whether the referenced schema is optional. + /// - Parameters: + /// - schema: The reference to check. + /// - components: The OpenAPI components for looking up references. + /// - cache: Memoised optionality by reference. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is optional, `false` otherwise. + func isOptional(_ ref: JSONReference, components: OpenAPI.Components, cache: inout [JSONReference: Bool]) throws -> Bool { + if let result = cache[ref] { + return result + } + let targetSchema = try components.lookup(ref) + cache[ref] = false // Pre-cache to treat directly recursive types as non-nullable. + let result = try isOptional(targetSchema, components: components, cache: &cache) + cache[ref] = result + return result + } + + /// Returns a Boolean value indicating whether the schema is optional at the root of any references. + /// - Parameters: + /// - schema: The reference to check. + /// - components: The OpenAPI components for looking up references. + /// - Throws: An error if there's an issue while checking the schema. + /// - Returns: `true` if the schema is an optional root, `false` otherwise. + func isOptionalRoot(_ schema: JSONSchema, components: OpenAPI.Components) throws -> Bool { + let directlyOptional = schema.nullable || !schema.required + switch schema.value { + case .null(_): + return true + case .reference(let ref, _): + let indirectlyOptional = try isOptional(ref, components: components) + return directlyOptional && !indirectlyOptional + default: + return directlyOptional + } + } + + /// Returns a Boolean value indicating whether the schema admits only explicit null values. + /// - Parameters: + /// - schema: The schema to check. + /// - Returns: `true` if the schema admits only explicit null values, `false` otherwise. + func isNull(_ schema: JSONSchema) -> Bool { + switch schema.value { + case .null(_): + return true + case let .fragment(core): + return core.format.jsonType == .null + default: + return false } } @@ -289,6 +382,7 @@ struct TypeMatcher { private static func _tryMatchBuiltinNonRecursive(for schema: JSONSchema.Schema) -> TypeUsage? { let typeName: TypeName switch schema { + case .null(_): typeName = TypeName.valueContainer case .boolean(_): typeName = .swift("Bool") case .number(let core, _): switch core.format { @@ -331,7 +425,7 @@ struct TypeMatcher { // arrays are already recursed-into by _tryMatchTypeRecursive // so just return nil here return nil - case .reference, .not, .all, .any, .one, .null: + case .reference, .not, .all, .any, .one: // never built-in return nil } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift index e4735b0b..e1fb6cc0 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/isSchemaSupported.swift @@ -132,7 +132,7 @@ extension FileTranslator { func isSchemaSupported(_ schema: JSONSchema, referenceStack: inout ReferenceStack) throws -> IsSchemaSupportedResult { switch schema.value { - case .string, .integer, .number, .boolean, + case .null, .string, .integer, .number, .boolean, // We mark any object as supported, even if it // has unsupported properties. // The code responsible for emitting an object is @@ -173,7 +173,7 @@ extension FileTranslator { schemas.filter(\.isReference), referenceStack: &referenceStack ) - case .not, .null: return .unsupported(reason: .schemaType, schema: schema) + case .not: return .unsupported(reason: .schemaType, schema: schema) } } diff --git a/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md b/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md index 1fe8b4ea..0d0aa806 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Development/Handling-nullable-schemas.md @@ -37,7 +37,7 @@ For example: ```yaml MyOptionalString: - type: [string, null] + type: [string, 'null'] ``` > The rule can be summarized as: `schema is optional := schema is nullable`, where being `nullable` is represented slightly differently between the two JSON Schema versions. @@ -75,7 +75,7 @@ MyPerson: name: type: string age: - type: [integer, null] + type: [integer, 'null'] required: - name - age # even though required, the nullability of the schema "wins" diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift index c76ec4c5..c67e34d9 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift @@ -104,6 +104,27 @@ class Test_TypeAssigner: Test_Core { } } + func testTypeNameForReferenceProperties() throws { + let parent = TypeName(swiftKeyPath: ["MyType"]) + let components: OpenAPI.Components = .init(schemas: [ + "SomeString": .string(), + "MaybeString": .one(of: [.reference(.component(named: "SomeString")), .null()]), + ]) + func assertTypeName(_ property: String, _ schema: JSONSchema, _ typeName: String, file: StaticString = #file, line: UInt = #line) throws { + let actual = try typeAssigner.typeUsage( + forObjectPropertyNamed: property, + withSchema: schema, + components: components, + inParent: parent + ).fullyQualifiedSwiftName + XCTAssertEqual(typeName, actual, file: file, line: line) + } + try assertTypeName("someString", .reference(.component(named: "SomeString")), "Components.Schemas.SomeString") + try assertTypeName("maybeString", .reference(.component(named: "MaybeString")), "Components.Schemas.MaybeString") + try assertTypeName("optionalSomeString", .reference(.component(named: "SomeString"), required: false), "Components.Schemas.SomeString?") + try assertTypeName("optionalMaybeString", .reference(.component(named: "MaybeString"), required: false), "Components.Schemas.MaybeString") + } + func testContentSwiftName() throws { let nameMaker = makeTranslator().typeAssigner.contentSwiftName let cases: [(String, String)] = [ diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift index a7e0b8e3..3d937c9c 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift @@ -216,6 +216,8 @@ final class Test_TypeMatcher: Test_Core { } static let optionalTestCases: [(JSONSchema, Bool)] = [ + // Explicit null. + (.null(), true), // A required string. (.string, false), (.string(required: true, nullable: false), false), @@ -227,10 +229,26 @@ final class Test_TypeMatcher: Test_Core { // A reference pointing to a required schema. (.reference(.component(named: "RequiredString")), false), (.reference(.component(named: "NullableString")), true), + + // Unknown type. + (.fragment(), false), + (.fragment(nullable: true), true), + + // References. + (.reference(.component(named: "List")), true), + (.reference(.component(named: "Loop")), false), ] func testOptionalSchemas() throws { let components = OpenAPI.Components(schemas: [ "RequiredString": .string, "NullableString": .string(nullable: true), + // Singlely linked list where null is an empty list. + "List": .one(of: [ + .null(), + .object(properties: ["next": .reference(.component(named: "List"), + required: true)])]), + // A non-empty circular linked list. + "Loop": .object(properties: ["next": .reference(.component(named: "Loop"), + required: true)]), ]) for (schema, expectedIsOptional) in Self.optionalTestCases { let actualIsOptional = try typeMatcher.isOptional(schema, components: components) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index ac8a417d..ecfe6c37 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -403,9 +403,9 @@ components: type: object properties: foo: - type: [array, null] + type: [array, 'null'] items: - type: [string, null] + type: [string, 'null'] CodeError: type: object properties: diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 52ad3b82..02f8be7c 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -192,7 +192,7 @@ final class SnippetBasedReferenceTests: XCTestCase { StringArrayNullableItems: type: array items: - type: [string, null] + type: [string, 'null'] """, """ public enum Schemas { @@ -213,7 +213,7 @@ final class SnippetBasedReferenceTests: XCTestCase { items: $ref: '#/components/schemas/NullableString' NullableString: - type: [string, null] + type: [string, 'null'] """, """ public enum Schemas { @@ -224,6 +224,25 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasNull() throws { + try self.assertSchemasTranslation( + """ + schemas: + Null: + type: "null" + NullArray: + type: array + items: + $ref: "#/components/schemas/Null" + """, + """ + public enum Schemas { + public typealias Null = OpenAPIRuntime.OpenAPIValueContainer? + public typealias NullArray = [Components.Schemas.Null] + } + """) + } + func testComponentsSchemasNullableStringProperty() throws { try self.assertSchemasTranslation( """ @@ -236,9 +255,9 @@ final class SnippetBasedReferenceTests: XCTestCase { fooRequired: type: string fooOptionalNullable: - type: [string, null] + type: [string, 'null'] fooRequiredNullable: - type: [string, null] + type: [string, 'null'] fooOptionalArray: type: array @@ -249,30 +268,30 @@ final class SnippetBasedReferenceTests: XCTestCase { items: type: string fooOptionalNullableArray: - type: [array, null] + type: [array, 'null'] items: type: string fooRequiredNullableArray: - type: [array, null] + type: [array, 'null'] items: type: string fooOptionalArrayOfNullableItems: type: array items: - type: [string, null] + type: [string, 'null'] fooRequiredArrayOfNullableItems: type: array items: - type: [string, null] + type: [string, 'null'] fooOptionalNullableArrayOfNullableItems: - type: [array, null] + type: [array, 'null'] items: - type: [string, null] + type: [string, 'null'] fooRequiredNullableArrayOfNullableItems: - type: [array, null] + type: [array, 'null'] items: - type: [string, null] + type: [string, 'null'] required: - fooRequired - fooRequiredNullable @@ -480,7 +499,7 @@ final class SnippetBasedReferenceTests: XCTestCase { MyRequiredString: type: string MyNullableString: - type: [string, null] + type: [string, 'null'] MyObject: type: object properties: @@ -501,17 +520,17 @@ final class SnippetBasedReferenceTests: XCTestCase { """ public enum Schemas { public typealias MyRequiredString = Swift.String - public typealias MyNullableString = Swift.String + public typealias MyNullableString = Swift.String? public struct MyObject: Codable, Hashable, Sendable { public var id: Swift.Int64 public var alias: Swift.String? public var requiredString: Components.Schemas.MyRequiredString - public var nullableString: Components.Schemas.MyNullableString? + public var nullableString: Components.Schemas.MyNullableString public init( id: Swift.Int64, alias: Swift.String? = nil, requiredString: Components.Schemas.MyRequiredString, - nullableString: Components.Schemas.MyNullableString? = nil + nullableString: Components.Schemas.MyNullableString ) { self.id = id self.alias = alias @@ -1742,6 +1761,31 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testOneOfRefOrNull() throws { + try self.assertSchemasTranslation( + """ + schemas: + SomeString: + type: string + NullableRef: + oneOf: + - $ref: '#/components/schemas/SomeString' + - type: 'null' + ArrayOfNullableRefs: + type: array + items: + $ref: '#/components/schemas/NullableRef' + """, + """ + public enum Schemas { + public typealias SomeString = Swift.String + public typealias NullableRef = Components.Schemas.SomeString? + public typealias ArrayOfNullableRefs = [Components.Schemas.NullableRef] + } + """ + ) + } + func testComponentsResponsesResponseNoBody() throws { try self.assertResponsesTranslation( """ @@ -2673,7 +2717,7 @@ final class SnippetBasedReferenceTests: XCTestCase { content: application/json: schema: - type: [string, null] + type: [string, 'null'] responses: default: description: Response @@ -2829,7 +2873,7 @@ final class SnippetBasedReferenceTests: XCTestCase { content: application/json: schema: - type: [string, null] + type: [string, 'null'] responses: default: description: Response