diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
index 456a8244f..17290dd1f 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
@@ -161,6 +161,24 @@ public OpenApiDocument CreateDeepCopy()
return new OpenApiDeepCopyContext().Copy(this);
}
+ ///
+ /// Applies semantic conversions needed to represent this document in a different OpenAPI specification version.
+ ///
+ /// The OpenAPI specification version this document currently represents.
+ /// The target OpenAPI specification version.
+ [System.Diagnostics.CodeAnalysis.Experimental("OPENAPI001")]
+ public void ApplySemanticConversions(OpenApiSpecVersion currentApiSpecVersion, OpenApiSpecVersion targetApiSpecVersion)
+ {
+ if (currentApiSpecVersion <= OpenApiSpecVersion.OpenApi3_0 && targetApiSpecVersion >= OpenApiSpecVersion.OpenApi3_1)
+ {
+ new OpenApiV3ToV3_1DeserializationConverter().Convert(this);
+ }
+ else if (currentApiSpecVersion >= OpenApiSpecVersion.OpenApi3_1 && targetApiSpecVersion <= OpenApiSpecVersion.OpenApi3_0)
+ {
+ new OpenApiV3_1ToV3SerializationConverter().Convert(this);
+ }
+ }
+
///
/// Serialize to an Open API document using the specified version.
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
index c24f66a5e..4977a9fa5 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
@@ -21,8 +21,6 @@ namespace Microsoft.OpenApi
///
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer, IDeepCopyable
{
- private static readonly IEnumerable s_singleNullElementList = [ JsonNullSentinel.JsonNull ];
-
///
public string? Title { get; set; }
@@ -536,21 +534,8 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
: Enum;
writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s));
- if (version == OpenApiSpecVersion.OpenApi3_0)
- {
- // If we have a schema that's only just { "type": "null" }, we serialize it as enum with null value.
- if (Type == JsonSchemaType.Null &&
- OneOf is not { Count: > 0 } &&
- AnyOf is not { Count: > 0 } &&
- AllOf is not { Count: > 0 } &&
- Enum is not { Count: > 0 })
- {
- writer.WriteOptionalCollection(OpenApiConstants.Enum, s_singleNullElementList, (nodeWriter, s) => nodeWriter.WriteAny(s));
- }
- }
-
// type
- var serializedTypeProperty = TrySerializeTypeProperty(writer, version);
+ TrySerializeTypeProperty(writer, version);
// allOf
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
@@ -595,13 +580,8 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
// nullable
- if (version == OpenApiSpecVersion.OpenApi3_0 && serializedTypeProperty)
+ if (version == OpenApiSpecVersion.OpenApi3_0)
{
- // https://spec.openapis.org/oas/v3.0.4.html#fixed-fields-20
- // This keyword only takes effect if type is explicitly defined within the same Schema Object.
- //
- // If the user explicitly set IsNullable to true, we serialize it even if redundant.
- // But if **we** are inferring it (from oneOf/anyOf), we don't serialize it when it's redundant.
SerializeNullable(writer, version);
}
diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
index 0967394cb..f4389a454 100644
--- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
@@ -3,12 +3,23 @@ const Microsoft.OpenApi.OpenApiConstants.JsonSchemaExamplesExtension = "x-jsonsc
const Microsoft.OpenApi.OpenApiConstants.OaiLicenseIdentifier = "x-oai-license-identifier" -> string!
[OPENAPI001]Microsoft.OpenApi.IDeepCopyable
[OPENAPI001]Microsoft.OpenApi.IDeepCopyable.CreateDeepCopy() -> T
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3_1ToV3SerializationConverter
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3_1ToV3SerializationConverter.Convert(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3_1ToV3SerializationConverter.Convert(Microsoft.OpenApi.OpenApiDocument! document) -> void
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3_1ToV3SerializationConverter.OpenApiV3_1ToV3SerializationConverter() -> void
+[OPENAPI001]override Microsoft.OpenApi.OpenApiV3_1ToV3SerializationConverter.Visit(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter.Convert(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter.Convert(Microsoft.OpenApi.OpenApiDocument! document) -> void
+[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter.OpenApiV3ToV3_1DeserializationConverter() -> void
+[OPENAPI001]override Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter.Visit(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
[OPENAPI001]Microsoft.OpenApi.OpenApiCallback.CreateDeepCopy() -> Microsoft.OpenApi.IOpenApiCallback!
[OPENAPI001]Microsoft.OpenApi.OpenApiCallbackReference.CreateDeepCopy() -> Microsoft.OpenApi.IOpenApiCallback!
[OPENAPI001]Microsoft.OpenApi.OpenApiComponents.CreateDeepCopy() -> Microsoft.OpenApi.OpenApiComponents!
[OPENAPI001]Microsoft.OpenApi.OpenApiContact.CreateDeepCopy() -> Microsoft.OpenApi.OpenApiContact!
[OPENAPI001]Microsoft.OpenApi.OpenApiDiscriminator.CreateDeepCopy() -> Microsoft.OpenApi.OpenApiDiscriminator!
[OPENAPI001]Microsoft.OpenApi.OpenApiDocument.CreateDeepCopy() -> Microsoft.OpenApi.OpenApiDocument!
+[OPENAPI001]Microsoft.OpenApi.OpenApiDocument.ApplySemanticConversions(Microsoft.OpenApi.OpenApiSpecVersion currentApiSpecVersion, Microsoft.OpenApi.OpenApiSpecVersion targetApiSpecVersion) -> void
[OPENAPI001]Microsoft.OpenApi.OpenApiEncoding.CreateDeepCopy() -> Microsoft.OpenApi.OpenApiEncoding!
[OPENAPI001]Microsoft.OpenApi.OpenApiExample.CreateDeepCopy() -> Microsoft.OpenApi.IOpenApiExample!
[OPENAPI001]Microsoft.OpenApi.OpenApiExampleReference.CreateDeepCopy() -> Microsoft.OpenApi.IOpenApiExample!
diff --git a/src/Microsoft.OpenApi/Services/OpenApiV3ToV3_1DeserializationConverter.cs b/src/Microsoft.OpenApi/Services/OpenApiV3ToV3_1DeserializationConverter.cs
new file mode 100644
index 000000000..e0866d73f
--- /dev/null
+++ b/src/Microsoft.OpenApi/Services/OpenApiV3ToV3_1DeserializationConverter.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.OpenApi;
+
+///
+/// Converts OpenAPI 3.0 deserialized schema constructs into OpenAPI 3.1 constructs.
+///
+[Experimental("OPENAPI001")]
+public sealed class OpenApiV3ToV3_1DeserializationConverter : OpenApiVisitorBase
+{
+ ///
+ /// Applies the conversion to an OpenAPI document.
+ ///
+ /// The document to convert.
+ public void Convert(OpenApiDocument document)
+ {
+ Utils.CheckArgumentNull(document);
+ new OpenApiWalker(this).Walk(document);
+ }
+
+ ///
+ /// Applies the conversion to a schema.
+ ///
+ /// The schema to convert.
+ public void Convert(IOpenApiSchema schema)
+ {
+ Utils.CheckArgumentNull(schema);
+ new OpenApiWalker(this).Walk(schema);
+ }
+
+ ///
+ public override void Visit(IOpenApiSchema schema)
+ {
+ if (schema is OpenApiSchema concreteSchema && ShouldConvertNullEnumToNullType(concreteSchema))
+ {
+ concreteSchema.Type = JsonSchemaType.Null;
+ concreteSchema.Enum = null;
+ }
+ }
+
+ private static bool ShouldConvertNullEnumToNullType(OpenApiSchema schema) =>
+ schema.Type is null &&
+ schema.OneOf is not { Count: > 0 } &&
+ schema.AnyOf is not { Count: > 0 } &&
+ schema.AllOf is not { Count: > 0 } &&
+ schema.Enum is { Count: 1 } enumValues &&
+ enumValues[0].IsJsonNullSentinel();
+}
diff --git a/src/Microsoft.OpenApi/Services/OpenApiV3_1ToV3SerializationConverter.cs b/src/Microsoft.OpenApi/Services/OpenApiV3_1ToV3SerializationConverter.cs
new file mode 100644
index 000000000..91bbacc19
--- /dev/null
+++ b/src/Microsoft.OpenApi/Services/OpenApiV3_1ToV3SerializationConverter.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Nodes;
+
+namespace Microsoft.OpenApi;
+
+///
+/// Converts OpenAPI 3.1 schema constructs into OpenAPI 3.0 serialization-compatible constructs.
+///
+[Experimental("OPENAPI001")]
+public sealed class OpenApiV3_1ToV3SerializationConverter : OpenApiVisitorBase
+{
+ ///
+ /// Applies the conversion to an OpenAPI document.
+ ///
+ /// The document to convert.
+ public void Convert(OpenApiDocument document)
+ {
+ Utils.CheckArgumentNull(document);
+ new OpenApiWalker(this).Walk(document);
+ }
+
+ ///
+ /// Applies the conversion to a schema.
+ ///
+ /// The schema to convert.
+ public void Convert(IOpenApiSchema schema)
+ {
+ Utils.CheckArgumentNull(schema);
+ new OpenApiWalker(this).Walk(schema);
+ }
+
+ ///
+ public override void Visit(IOpenApiSchema schema)
+ {
+ if (schema is OpenApiSchema concreteSchema && ShouldConvertNullTypeToNullEnum(concreteSchema))
+ {
+ concreteSchema.Enum = new List
+ {
+ JsonNullSentinel.JsonNull
+ };
+ concreteSchema.Type = null;
+ }
+ }
+
+ private static bool ShouldConvertNullTypeToNullEnum(OpenApiSchema schema) =>
+ schema.Type == JsonSchemaType.Null &&
+ schema.OneOf is not { Count: > 0 } &&
+ schema.AnyOf is not { Count: > 0 } &&
+ schema.AllOf is not { Count: > 0 } &&
+ schema.Enum is not { Count: > 0 };
+}
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
index db4de6355..9ac9458d4 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
+#pragma warning disable OPENAPI001
+
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -963,9 +965,7 @@ public async Task SerializeOneOfWithNullAsV3ShouldUseNullableAsync()
{
"oneOf": [
{
- "enum": [
- null
- ]
+ "nullable": true
},
{
"maxLength": 10,
@@ -1007,9 +1007,7 @@ public async Task SerializeOneOfWithNullAndMultipleSchemasAsV3ShouldMarkItAsNull
{
"oneOf": [
{
- "enum": [
- null
- ]
+ "nullable": true
},
{
"type": "string"
@@ -1059,9 +1057,7 @@ public async Task SerializeAnyOfWithNullAsV3ShouldUseNullableAsync()
{
"anyOf": [
{
- "enum": [
- null
- ]
+ "nullable": true
},
{
"type": "object",
@@ -1106,9 +1102,7 @@ public async Task SerializeAnyOfWithNullAndMultipleSchemasAsV3ShouldApplyNullabl
{
"anyOf": [
{
- "enum": [
- null
- ]
+ "nullable": true
},
{
"minLength": 1,
@@ -1151,9 +1145,7 @@ public async Task SerializeOneOfWithOnlyNullAsV3ShouldJustBeNullableAsync()
{
"oneOf": [
{
- "enum": [
- null
- ]
+ "nullable": true
}
]
}
@@ -1249,6 +1241,244 @@ public async Task SerializeOneOfWithNullAndRefAsV3ShouldUseNullableAsync()
var v3Schema = outputStringWriter.GetStringBuilder().ToString();
+ var expectedV3Schema =
+ """
+ {
+ "oneOf": [
+ {
+ "nullable": true
+ },
+ {
+ "$ref": "#/components/schemas/Pet"
+ }
+ ]
+ }
+ """;
+
+ // Assert
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
+ }
+
+ [Fact]
+ public async Task SerializeOneOfWithNullAsConvertedV3ShouldUseNullEnumAsync()
+ {
+ var schema = new OpenApiSchema
+ {
+ OneOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.Null },
+ new OpenApiSchema
+ {
+ Type = JsonSchemaType.String,
+ MaxLength = 10
+ }
+ }
+ };
+
+ var v3Schema = await SerializeConvertedCloneAsV3Async(schema);
+
+ var expectedV3Schema =
+ """
+ {
+ "oneOf": [
+ {
+ "enum": [
+ null
+ ]
+ },
+ {
+ "maxLength": 10,
+ "type": "string"
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
+ }
+
+ [Fact]
+ public async Task SerializeOneOfWithNullAndMultipleSchemasAsConvertedV3ShouldUseNullEnum()
+ {
+ var schema = new OpenApiSchema
+ {
+ OneOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.Null },
+ new OpenApiSchema { Type = JsonSchemaType.String },
+ new OpenApiSchema { Type = JsonSchemaType.Number },
+ }
+ };
+
+ var v3Schema = await SerializeConvertedCloneAsV3Async(schema);
+
+ var expectedV3Schema =
+ """
+ {
+ "oneOf": [
+ {
+ "enum": [
+ null
+ ]
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
+ }
+
+ [Fact]
+ public async Task SerializeAnyOfWithNullAsConvertedV3ShouldUseNullEnumAsync()
+ {
+ var schema = new OpenApiSchema
+ {
+ AnyOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.Null },
+ new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["id"] = new OpenApiSchema { Type = JsonSchemaType.Integer }
+ }
+ }
+ }
+ };
+
+ var v3Schema = await SerializeConvertedCloneAsV3Async(schema);
+
+ var expectedV3Schema =
+ """
+ {
+ "anyOf": [
+ {
+ "enum": [
+ null
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ """;
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
+ }
+
+ [Fact]
+ public async Task SerializeAnyOfWithNullAndMultipleSchemasAsConvertedV3ShouldUseNullEnum()
+ {
+ var schema = new OpenApiSchema
+ {
+ AnyOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.Null },
+ new OpenApiSchema { Type = JsonSchemaType.String, MinLength = 1 },
+ new OpenApiSchema { Type = JsonSchemaType.Integer }
+ }
+ };
+
+ var v3Schema = await SerializeConvertedCloneAsV3Async(schema);
+
+ var expectedV3Schema =
+ """
+ {
+ "anyOf": [
+ {
+ "enum": [
+ null
+ ]
+ },
+ {
+ "minLength": 1,
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
+ }
+
+ [Fact]
+ public async Task SerializeOneOfWithOnlyNullAsConvertedV3ShouldUseNullEnumAsync()
+ {
+ var schema = new OpenApiSchema
+ {
+ OneOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.Null }
+ }
+ };
+
+ var v3Schema = await SerializeConvertedCloneAsV3Async(schema);
+
+ var expectedV3Schema =
+ """
+ {
+ "oneOf": [
+ {
+ "enum": [
+ null
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
+ }
+
+ [Fact]
+ public async Task SerializeOneOfWithNullAndRefAsConvertedV3ShouldUseNullEnumAsync()
+ {
+ var document = new OpenApiDocument
+ {
+ Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ ["Pet"] = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["id"] = new OpenApiSchema { Type = JsonSchemaType.Integer },
+ ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }
+ }
+ }
+ }
+ }
+ };
+ document.Workspace.RegisterComponents(document);
+ var schemaRef = new OpenApiSchemaReference("Pet", document);
+ var schema = new OpenApiSchema
+ {
+ OneOf = new List
+ {
+ new OpenApiSchema { Type = JsonSchemaType.Null },
+ schemaRef
+ }
+ };
+
+ var v3Schema = await SerializeConvertedCloneAsV3Async(schema);
+
var expectedV3Schema =
"""
{
@@ -1265,10 +1495,140 @@ public async Task SerializeOneOfWithNullAndRefAsV3ShouldUseNullableAsync()
}
""";
- // Assert
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema)));
}
+ [Fact]
+ public async Task ConvertSingleNullEnumSchemaToV31TypeNullAsync()
+ {
+ var schema = new OpenApiSchema
+ {
+ Enum = new List
+ {
+ JsonNullSentinel.JsonNull
+ }
+ };
+
+ new OpenApiV3ToV3_1DeserializationConverter().Convert(schema);
+
+ Assert.Equal(JsonSchemaType.Null, schema.Type);
+ Assert.Null(schema.Enum);
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);
+ var expected = """
+ {
+ "type": "null"
+ }
+ """;
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public void ConvertSingleNullEnumNestedInDocumentToV31TypeNull()
+ {
+ var nullSchema = new OpenApiSchema
+ {
+ Enum = new List
+ {
+ JsonNullSentinel.JsonNull
+ }
+ };
+ var document = new OpenApiDocument
+ {
+ Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ ["NullableName"] = new OpenApiSchema
+ {
+ OneOf = new List
+ {
+ nullSchema,
+ new OpenApiSchema { Type = JsonSchemaType.String }
+ }
+ }
+ }
+ }
+ };
+
+ new OpenApiV3ToV3_1DeserializationConverter().Convert(document);
+
+ Assert.Equal(JsonSchemaType.Null, nullSchema.Type);
+ Assert.Null(nullSchema.Enum);
+ }
+
+ [Fact]
+ public void ConvertDoesNotChangeEnumWithNullAndOtherValues()
+ {
+ var schema = new OpenApiSchema
+ {
+ Enum = new List
+ {
+ JsonNullSentinel.JsonNull,
+ JsonValue.Create("A")
+ }
+ };
+
+ new OpenApiV3ToV3_1DeserializationConverter().Convert(schema);
+
+ Assert.Null(schema.Type);
+ Assert.NotNull(schema.Enum);
+ Assert.Equal(2, schema.Enum.Count);
+ }
+
+ [Fact]
+ public void ApplySemanticConversionsFromV3ToV31ConvertsNullEnumToNullType()
+ {
+ var nullSchema = new OpenApiSchema
+ {
+ Enum = new List
+ {
+ JsonNullSentinel.JsonNull
+ }
+ };
+ var document = new OpenApiDocument
+ {
+ Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ ["NullableValue"] = nullSchema
+ }
+ }
+ };
+
+ document.ApplySemanticConversions(OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi3_1);
+
+ Assert.Equal(JsonSchemaType.Null, nullSchema.Type);
+ Assert.Null(nullSchema.Enum);
+ }
+
+ [Fact]
+ public void ApplySemanticConversionsFromV31ToV3ConvertsNullTypeToNullEnum()
+ {
+ var nullSchema = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Null
+ };
+ var document = new OpenApiDocument
+ {
+ Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ ["NullableValue"] = nullSchema
+ }
+ }
+ };
+
+ document.ApplySemanticConversions(OpenApiSpecVersion.OpenApi3_1, OpenApiSpecVersion.OpenApi3_0);
+
+ Assert.Null(nullSchema.Type);
+ var enumValues = nullSchema.Enum;
+ Assert.NotNull(enumValues);
+ Assert.Single(enumValues);
+ Assert.True(enumValues[0].IsJsonNullSentinel());
+ }
+
[Fact]
public async Task SerializeContainsKeywordsAsV31Works()
{
@@ -2035,15 +2395,32 @@ public void DeserializeContainsExtensionsInV3AssignsContainsProperties()
[Fact]
public async Task SerializeNullableEnumWith3_0()
{
- // https://spec.openapis.org/oas/v3.0.4.html#fixed-fields-20
- // Documentation for nullable states:
- // This keyword only takes effect if type is explicitly defined within the same Schema Object.
- // So, we want to ensure that we emit the type property if we will be adding nullable property.
- // In addition, we need to still keep 'null' in the enum array.
- // Otherwise, validators will consider null as invalid even if nullable is set to true.
- // It's unclear if it's an issue of the validators or not, but it's safer to do it that way.
var schema = CreateNullableEnumSchema();
var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "oneOf": [
+ {
+ "nullable": true
+ },
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeNullableEnumWithConverted3_0()
+ {
+ var schema = CreateNullableEnumSchema();
+ var result = await SerializeConvertedCloneAsV3Async(schema);
var expected = """
{
"oneOf": [
@@ -2095,6 +2472,38 @@ public async Task SerializeNullableTypeWith3_0()
{
var schema = CreateTypeNullSchema();
var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "nullable": true
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesIncludingNullWith3_0EmitsNullable()
+ {
+ var schema = new OpenApiSchema
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null
+ };
+
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "nullable": true
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeNullableTypeWithConverted3_0()
+ {
+ var schema = CreateTypeNullSchema();
+ var result = await SerializeConvertedCloneAsV3Async(schema);
var expected = """
{
"enum": [
@@ -2121,6 +2530,18 @@ public async Task SerializeNullableTypeWith3_1_And_Later(OpenApiSpecVersion vers
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
}
+ private static async Task SerializeConvertedCloneAsV3Async(OpenApiSchema schema)
+ {
+ var convertedSchema = Assert.IsType(schema.CreateDeepCopy());
+ new OpenApiV3_1ToV3SerializationConverter().Convert(convertedSchema);
+
+ var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture);
+ var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false });
+ convertedSchema.SerializeAsV3(writer);
+ await writer.FlushAsync();
+ return outputStringWriter.GetStringBuilder().ToString();
+ }
+
private OpenApiSchema CreateNullableEnumSchema()
{
var schema = new OpenApiSchema();