Skip to content

Commit b42d7bd

Browse files
committed
Emit nullable/x-nullable property at original location, simplify writing schema type
1 parent f0ff148 commit b42d7bd

10 files changed

+87
-109
lines changed

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

+58-99
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ public string? ExclusiveMinimum
104104
/// <inheritdoc />
105105
public JsonSchemaType? Type { get; set; }
106106

107+
// x-nullable is filtered out by deserializers, but keep the check here in case it gets added from user code.
108+
private bool IsNullable =>
109+
(Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
110+
Extensions is not null &&
111+
Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) &&
112+
nullExtRawValue is JsonNodeExtension { Node: JsonNode jsonNode } &&
113+
jsonNode.GetValueKind() is JsonValueKind.True;
114+
107115
/// <inheritdoc />
108116
public string? Const { get; set; }
109117

@@ -437,7 +445,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
437445
writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (nodeWriter, s) => nodeWriter.WriteAny(s));
438446

439447
// type
440-
SerializeTypeProperty(Type, writer, version);
448+
SerializeTypeProperty(writer, version);
441449

442450
// allOf
443451
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
@@ -479,6 +487,12 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
479487
// default
480488
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
481489

490+
// nullable
491+
if (version == OpenApiSpecVersion.OpenApi3_0)
492+
{
493+
SerializeNullable(writer, version);
494+
}
495+
482496
// discriminator
483497
writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, callback);
484498

@@ -619,7 +633,7 @@ private void SerializeAsV2(
619633
writer.WriteStartObject();
620634

621635
// type
622-
SerializeTypeProperty(Type, writer, OpenApiSpecVersion.OpenApi2_0);
636+
SerializeTypeProperty(writer, OpenApiSpecVersion.OpenApi2_0);
623637

624638
// description
625639
writer.WriteProperty(OpenApiConstants.Description, Description);
@@ -742,66 +756,36 @@ private void SerializeAsV2(
742756
// example
743757
writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
744758

759+
// x-nullable extension
760+
SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0);
761+
745762
// extensions
746763
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0);
747764

748765
writer.WriteEndObject();
749766
}
750767

751-
private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version)
768+
private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version)
752769
{
753-
// check whether nullable is true for upcasting purposes
754-
var isNullable = (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
755-
Extensions is not null &&
756-
Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) &&
757-
nullExtRawValue is JsonNodeExtension { Node: JsonNode jsonNode } &&
758-
jsonNode.GetValueKind() is JsonValueKind.True;
759-
if (type is null)
770+
if (Type is null)
760771
{
761-
if (version is OpenApiSpecVersion.OpenApi3_0 && isNullable)
762-
{
763-
writer.WriteProperty(OpenApiConstants.Nullable, true);
764-
}
772+
return;
765773
}
766-
else if (!HasMultipleTypes(type.Value))
767-
{
768-
switch (version)
769-
{
770-
case OpenApiSpecVersion.OpenApi3_1 when isNullable:
771-
UpCastSchemaTypeToV31(type.Value, writer);
772-
break;
773-
case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value == JsonSchemaType.Null:
774-
writer.WriteProperty(OpenApiConstants.Nullable, true);
775-
break;
776-
case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value != JsonSchemaType.Null:
777-
writer.WriteProperty(OpenApiConstants.Nullable, true);
778-
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToFirstIdentifier());
779-
break;
780-
default:
781-
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToFirstIdentifier());
782-
break;
783-
}
784-
}
785-
else
774+
775+
var unifiedType = IsNullable ? Type.Value | JsonSchemaType.Null : Type.Value;
776+
var typeWithoutNull = unifiedType & ~JsonSchemaType.Null;
777+
778+
switch (version)
786779
{
787-
// type
788-
if (version is OpenApiSpecVersion.OpenApi2_0 || version is OpenApiSpecVersion.OpenApi3_0)
789-
{
790-
DowncastTypeArrayToV2OrV3(type.Value, writer, version);
791-
}
792-
else
793-
{
794-
var list = (from JsonSchemaType flag in jsonSchemaTypeValues
795-
where type.Value.HasFlag(flag)
796-
select flag).ToList();
797-
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) =>
780+
case OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0:
781+
if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
798782
{
799-
foreach (var item in s.ToIdentifiers())
800-
{
801-
w.WriteValue(item);
802-
}
803-
});
804-
}
783+
writer.WriteProperty(OpenApiConstants.Type, Type.Value.ToFirstIdentifier());
784+
}
785+
break;
786+
default:
787+
WriteUnifiedSchemaType(unifiedType, writer);
788+
break;
805789
}
806790
}
807791

@@ -813,20 +797,17 @@ private static bool IsPowerOfTwo(int x)
813797
private static bool HasMultipleTypes(JsonSchemaType schemaType)
814798
{
815799
var schemaTypeNumeric = (int)schemaType;
816-
return !IsPowerOfTwo(schemaTypeNumeric) && // Boolean, Integer, Number, String, Array, Object
817-
schemaTypeNumeric != (int)JsonSchemaType.Null;
800+
return !IsPowerOfTwo(schemaTypeNumeric);
818801
}
819802

820-
private static void UpCastSchemaTypeToV31(JsonSchemaType type, IOpenApiWriter writer)
803+
private static void WriteUnifiedSchemaType(JsonSchemaType type, IOpenApiWriter writer)
821804
{
822-
// create a new array and insert the type and "null" as values
823-
var temporaryType = type | JsonSchemaType.Null;
824-
var list = (from JsonSchemaType flag in jsonSchemaTypeValues// Check if the flag is set in 'type' using a bitwise AND operation
825-
where temporaryType.HasFlag(flag)
826-
select flag.ToFirstIdentifier()).ToList();
827-
if (list.Count > 1)
805+
var array = (from JsonSchemaType flag in jsonSchemaTypeValues
806+
where type.HasFlag(flag)
807+
select flag.ToFirstIdentifier()).ToArray();
808+
if (array.Length > 1)
828809
{
829-
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) =>
810+
writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) =>
830811
{
831812
if (!string.IsNullOrEmpty(s) && s is not null)
832813
{
@@ -836,54 +817,32 @@ where temporaryType.HasFlag(flag)
836817
}
837818
else
838819
{
839-
writer.WriteProperty(OpenApiConstants.Type, list[0]);
820+
writer.WriteProperty(OpenApiConstants.Type, array[0]);
840821
}
841822
}
842823

843-
#if NET5_0_OR_GREATER
844-
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues<JsonSchemaType>();
845-
#else
846-
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues(typeof(JsonSchemaType));
847-
#endif
848-
849-
private static void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter writer, OpenApiSpecVersion version)
824+
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
850825
{
851-
/* If the array has one non-null value, emit Type as string
852-
* If the array has one null value, emit x-nullable as true
853-
* If the array has two values, one null and one non-null, emit Type as string and x-nullable as true
854-
* If the array has more than two values or two non-null values, do not emit type
855-
* */
856-
857-
var nullableProp = version.Equals(OpenApiSpecVersion.OpenApi2_0)
858-
? OpenApiConstants.NullableExtension
859-
: OpenApiConstants.Nullable;
860-
861-
if (!HasMultipleTypes(schemaType & ~JsonSchemaType.Null) && (schemaType & JsonSchemaType.Null) == JsonSchemaType.Null) // checks for two values and one is null
862-
{
863-
foreach (JsonSchemaType flag in jsonSchemaTypeValues)
864-
{
865-
// Skip if the flag is not set or if it's the Null flag
866-
if (schemaType.HasFlag(flag) && flag != JsonSchemaType.Null)
867-
{
868-
// Write the non-null flag value to the writer
869-
writer.WriteProperty(OpenApiConstants.Type, flag.ToFirstIdentifier());
870-
}
871-
}
872-
writer.WriteProperty(nullableProp, true);
873-
}
874-
else if (!HasMultipleTypes(schemaType))
826+
if (IsNullable)
875827
{
876-
if (schemaType is JsonSchemaType.Null)
877-
{
878-
writer.WriteProperty(nullableProp, true);
879-
}
880-
else
828+
switch (version)
881829
{
882-
writer.WriteProperty(OpenApiConstants.Type, schemaType.ToFirstIdentifier());
830+
case OpenApiSpecVersion.OpenApi2_0:
831+
writer.WriteProperty(OpenApiConstants.NullableExtension, true);
832+
break;
833+
case OpenApiSpecVersion.OpenApi3_0:
834+
writer.WriteProperty(OpenApiConstants.Nullable, true);
835+
break;
883836
}
884837
}
885838
}
886839

840+
#if NET5_0_OR_GREATER
841+
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues<JsonSchemaType>();
842+
#else
843+
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues(typeof(JsonSchemaType));
844+
#endif
845+
887846
/// <inheritdoc/>
888847
public IOpenApiSchema CreateShallowCopy()
889848
{

src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs

+10
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,16 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu
271271
propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields, hostDocument);
272272
}
273273

274+
if (schema.Extensions is not null && schema.Extensions.ContainsKey(OpenApiConstants.NullableExtension))
275+
{
276+
if (schema.Type.HasValue)
277+
schema.Type |= JsonSchemaType.Null;
278+
else
279+
schema.Type = JsonSchemaType.Null;
280+
281+
schema.Extensions.Remove(OpenApiConstants.NullableExtension);
282+
}
283+
274284
return schema;
275285
}
276286
}

src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs

+10
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,16 @@ public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocu
307307
propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields, hostDocument);
308308
}
309309

310+
if (schema.Extensions is not null && schema.Extensions.ContainsKey(OpenApiConstants.NullableExtension))
311+
{
312+
if (schema.Type.HasValue)
313+
schema.Type |= JsonSchemaType.Null;
314+
else
315+
schema.Type = JsonSchemaType.Null;
316+
317+
schema.Extensions.Remove(OpenApiConstants.NullableExtension);
318+
}
319+
310320
return schema;
311321
}
312322
}

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,7 @@ public async Task SerializeV2SchemaWithNullableExtensionAsV31Works()
384384
// Arrange
385385
var expected = @"type:
386386
- 'null'
387-
- string
388-
x-nullable: true";
387+
- string";
389388

390389
var path = Path.Combine(SampleFolderPath, "schemaWithNullableExtension.yaml");
391390

@@ -407,7 +406,7 @@ public void SerializeSchemaWithTypeArrayAndNullableDoesntEmitType()
407406
- ""int""
408407
nullable: true";
409408

410-
var expected = @"{ }";
409+
var expected = @"x-nullable: true";
411410

412411
var schema = OpenApiModelFactory.Parse<OpenApiSchema>(input, OpenApiSpecVersion.OpenApi3_1, new(), out _, "yaml", SettingsFixture.ReaderSettings);
413412

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=False.verified.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"minimum": 10,
66
"exclusiveMinimum": true,
77
"type": "integer",
8-
"nullable": true,
98
"default": 15,
9+
"nullable": true,
1010
"externalDocs": {
1111
"url": "http://example.com/externalDocs"
1212
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","nullable":true,"default":15,"externalDocs":{"url":"http://example.com/externalDocs"}}
1+
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","default":15,"nullable":true,"externalDocs":{"url":"http://example.com/externalDocs"}}

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=False.verified.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"minimum": 10,
66
"exclusiveMinimum": true,
77
"type": "integer",
8-
"nullable": true,
98
"default": 15,
9+
"nullable": true,
1010
"externalDocs": {
1111
"url": "http://example.com/externalDocs"
1212
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","nullable":true,"default":15,"externalDocs":{"url":"http://example.com/externalDocs"}}
1+
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","default":15,"nullable":true,"externalDocs":{"url":"http://example.com/externalDocs"}}

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeSchemaWRequiredPropertiesAsV2JsonWorksAsync_produceTerseOutput=False.verified.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"type": "object",
3-
"x-nullable": true,
43
"title": "title1",
54
"required": [
65
"property1"
@@ -39,5 +38,6 @@
3938
},
4039
"externalDocs": {
4140
"url": "http://example.com/externalDocs"
42-
}
41+
},
42+
"x-nullable": true
4343
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"type":"object","x-nullable":true,"title":"title1","required":["property1"],"properties":{"property1":{"required":["property3"],"properties":{"property2":{"type":"integer"},"property3":{"type":"string","maxLength":15}}},"property4":{"properties":{"property5":{"properties":{"property6":{"type":"boolean"}}},"property7":{"type":"string","minLength":2}},"readOnly":true}},"externalDocs":{"url":"http://example.com/externalDocs"}}
1+
{"type":"object","title":"title1","required":["property1"],"properties":{"property1":{"required":["property3"],"properties":{"property2":{"type":"integer"},"property3":{"type":"string","maxLength":15}}},"property4":{"properties":{"property5":{"properties":{"property6":{"type":"boolean"}}},"property7":{"type":"string","minLength":2}},"readOnly":true}},"externalDocs":{"url":"http://example.com/externalDocs"},"x-nullable":true}

0 commit comments

Comments
 (0)