Skip to content

Commit 44375a6

Browse files
committed
feat: moves additional semantic conversions to converters instead of serialization
1 parent 269b050 commit 44375a6

16 files changed

Lines changed: 635 additions & 111 deletions

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,25 @@ public OpenApiDocument CreateDeepCopy()
169169
[System.Diagnostics.CodeAnalysis.Experimental("OPENAPI001")]
170170
public void ApplySemanticConversions(OpenApiSpecVersion currentApiSpecVersion, OpenApiSpecVersion targetApiSpecVersion)
171171
{
172+
if (currentApiSpecVersion == OpenApiSpecVersion.OpenApi2_0 && targetApiSpecVersion >= OpenApiSpecVersion.OpenApi3_0)
173+
{
174+
new OpenApiV2ToV3DeserializationConverter().Convert(this);
175+
}
176+
172177
if (currentApiSpecVersion <= OpenApiSpecVersion.OpenApi3_0 && targetApiSpecVersion >= OpenApiSpecVersion.OpenApi3_1)
173178
{
174179
new OpenApiV3ToV3_1DeserializationConverter().Convert(this);
175180
}
176-
else if (currentApiSpecVersion >= OpenApiSpecVersion.OpenApi3_1 && targetApiSpecVersion <= OpenApiSpecVersion.OpenApi3_0)
181+
182+
if (currentApiSpecVersion >= OpenApiSpecVersion.OpenApi3_1 && targetApiSpecVersion <= OpenApiSpecVersion.OpenApi3_0)
177183
{
178184
new OpenApiV3_1ToV3SerializationConverter().Convert(this);
179185
}
186+
187+
if (currentApiSpecVersion >= OpenApiSpecVersion.OpenApi3_0 && targetApiSpecVersion == OpenApiSpecVersion.OpenApi2_0)
188+
{
189+
new OpenApiV3ToV2SerializationConverter().Convert(this);
190+
}
180191
}
181192

182193
/// <summary>

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 85 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ public string? ExclusiveMinimum
112112
private bool HasNullType
113113
=> Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null);
114114

115+
internal bool IsNullableCompatibility { get; set; }
116+
115117
internal bool WasConstExplicitlySet { get; private set; }
116118

117119
/// <inheritdoc />
@@ -368,6 +370,7 @@ internal OpenApiSchema(IOpenApiSchema schema)
368370
{
369371
IsExclusiveMaximum = eMSchema.IsExclusiveMaximum;
370372
IsExclusiveMinimum = eMSchema.IsExclusiveMinimum;
373+
IsNullableCompatibility = eMSchema.IsNullableCompatibility;
371374
}
372375
Type = schema.Type ?? Type;
373376
Format = schema.Format ?? Format;
@@ -436,12 +439,6 @@ private static void SerializeBounds(IOpenApiWriter writer, OpenApiSpecVersion ve
436439
writer.WritePropertyName(exclusivePropertyName);
437440
writer.WriteRaw(exclusiveValue);
438441
}
439-
else if (isExclusiveValue == true && !string.IsNullOrEmpty(value) && value is not null)
440-
{
441-
// came from parsing an old document
442-
writer.WritePropertyName(exclusivePropertyName);
443-
writer.WriteRaw(value);
444-
}
445442
else if (!string.IsNullOrEmpty(value) && value is not null)
446443
{
447444
// was explicitly set in the document or object model
@@ -451,14 +448,7 @@ private static void SerializeBounds(IOpenApiWriter writer, OpenApiSpecVersion ve
451448
}
452449
else
453450
{
454-
if (!string.IsNullOrEmpty(exclusiveValue) && exclusiveValue is not null)
455-
{
456-
// was explicitly set in a new document being downcast or object model
457-
writer.WritePropertyName(propertyName);
458-
writer.WriteRaw(exclusiveValue);
459-
writer.WriteProperty(isExclusivePropertyName, true);
460-
}
461-
else if (!string.IsNullOrEmpty(value) && value is not null)
451+
if (!string.IsNullOrEmpty(value) && value is not null)
462452
{
463453
// came from parsing an old document, we're just mirroring the information
464454
writer.WritePropertyName(propertyName);
@@ -487,11 +477,11 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
487477

488478
// maximum
489479
// exclusiveMaximum
490-
SerializeBounds(writer, version, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, Maximum, ExclusiveMaximum, IsExclusiveMaximum);
480+
SerializeBounds(writer, version, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, _maximum, ExclusiveMaximum, IsExclusiveMaximum);
491481

492482
// minimum
493483
// exclusiveMinimum
494-
SerializeBounds(writer, version, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, Minimum, ExclusiveMinimum, IsExclusiveMinimum);
484+
SerializeBounds(writer, version, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, _minimum, ExclusiveMinimum, IsExclusiveMinimum);
495485

496486
// maxLength
497487
writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength);
@@ -527,12 +517,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
527517
});
528518

529519
// enum
530-
var enumValue = Enum is not { Count: > 0 }
531-
&& WasConstExplicitlySet
532-
&& version < OpenApiSpecVersion.OpenApi3_1
533-
? new List<JsonNode> { JsonValue.Create(Const)! }
534-
: Enum;
535-
writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s));
520+
writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (nodeWriter, s) => nodeWriter.WriteAny(s));
536521

537522
// type
538523
TrySerializeTypeProperty(writer, version);
@@ -601,10 +586,12 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
601586
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback);
602587

603588
// example
589+
#pragma warning disable CS0618
604590
writer.WriteOptionalObject(
605591
OpenApiConstants.Example,
606-
version < OpenApiSpecVersion.OpenApi3_1 ? GetCompatibilityExample() : Example,
592+
Example,
607593
(w, e) => w.WriteAny(e));
594+
#pragma warning restore CS0618
608595

609596
// deprecated
610597
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);
@@ -730,7 +717,6 @@ private void WriteV3CompatibilityKeywords(IOpenApiWriter writer, Action<IOpenApi
730717
writer.WriteOptionalObject(OpenApiConstants.IfExtension, If, callback);
731718
writer.WriteOptionalObject(OpenApiConstants.ThenExtension, Then, callback);
732719
writer.WriteOptionalObject(OpenApiConstants.ElseExtension, Else, callback);
733-
writer.WriteOptionalCollection(OpenApiConstants.JsonSchemaExamplesExtension, GetCompatibilityExamplesExtension(), (nodeWriter, s) => nodeWriter.WriteAny(s));
734720
}
735721

736722
internal void WriteAsItemsProperties(IOpenApiWriter writer)
@@ -757,11 +743,11 @@ internal void WriteAsItemsProperties(IOpenApiWriter writer)
757743

758744
// maximum
759745
// exclusiveMaximum
760-
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, Maximum, ExclusiveMaximum, IsExclusiveMaximum);
746+
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, _maximum, ExclusiveMaximum, IsExclusiveMaximum);
761747

762748
// minimum
763749
// exclusiveMinimum
764-
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, Minimum, ExclusiveMinimum, IsExclusiveMinimum);
750+
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, _minimum, ExclusiveMinimum, IsExclusiveMinimum);
765751

766752
// maxLength
767753
writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength);
@@ -790,15 +776,7 @@ internal void WriteAsItemsProperties(IOpenApiWriter writer)
790776

791777
private void WriteFormatProperty(IOpenApiWriter writer)
792778
{
793-
var formatToWrite = Format;
794-
if (string.IsNullOrEmpty(formatToWrite))
795-
{
796-
formatToWrite = AllOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format ??
797-
AnyOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format ??
798-
OneOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format;
799-
}
800-
801-
writer.WriteProperty(OpenApiConstants.Format, formatToWrite);
779+
writer.WriteProperty(OpenApiConstants.Format, Format);
802780
}
803781

804782
/// <summary>
@@ -837,11 +815,11 @@ private void SerializeAsV2(
837815

838816
// maximum
839817
// exclusiveMaximum
840-
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, Maximum, ExclusiveMaximum, IsExclusiveMaximum);
818+
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Maximum, OpenApiConstants.ExclusiveMaximum, OpenApiConstants.V31ExclusiveMaximum, _maximum, ExclusiveMaximum, IsExclusiveMaximum);
841819

842820
// minimum
843821
// exclusiveMinimum
844-
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, Minimum, ExclusiveMinimum, IsExclusiveMinimum);
822+
SerializeBounds(writer, OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Minimum, OpenApiConstants.ExclusiveMinimum, OpenApiConstants.V31ExclusiveMinimum, _minimum, ExclusiveMinimum, IsExclusiveMinimum);
845823

846824
// maxLength
847825
writer.WriteProperty(OpenApiConstants.MaxLength, MaxLength);
@@ -877,10 +855,7 @@ private void SerializeAsV2(
877855
});
878856

879857
// enum
880-
var enumValue = Enum is not { Count: > 0 } && WasConstExplicitlySet
881-
? new List<JsonNode> { JsonValue.Create(Const)! }
882-
: Enum;
883-
writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s));
858+
writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (nodeWriter, s) => nodeWriter.WriteAny(s));
884859

885860
// items
886861
writer.WriteOptionalObject(OpenApiConstants.Items, Items, (w, s) => s.SerializeAsV2(w));
@@ -945,7 +920,6 @@ private void SerializeAsV2(
945920

946921
// example
947922
#pragma warning disable CS0618
948-
writer.WriteOptionalObject(OpenApiConstants.Example, GetCompatibilityExample(), (w, e) => w.WriteAny(e));
949923
writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
950924
#pragma warning restore CS0618
951925

@@ -978,7 +952,6 @@ private void SerializeAsV2(
978952
writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, (w, s) => s.SerializeAsV2(w));
979953
}
980954

981-
writer.WriteOptionalCollection(OpenApiConstants.JsonSchemaExamplesExtension, GetCompatibilityExamplesExtension(), (nodeWriter, s) => nodeWriter.WriteAny(s));
982955

983956
// extensions
984957
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0);
@@ -999,10 +972,19 @@ private bool TrySerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion
999972
switch (version)
1000973
{
1001974
case OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0:
1002-
var typeWithoutNull = typeToUse.Value & ~JsonSchemaType.Null;
1003-
if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
975+
if (typeToUse.Value.HasFlag(JsonSchemaType.Null))
976+
{
977+
if (!IsNullableCompatibility)
978+
{
979+
return false;
980+
}
981+
982+
typeToUse &= ~JsonSchemaType.Null;
983+
}
984+
985+
if (typeToUse != 0 && !HasMultipleTypes(typeToUse.Value))
1004986
{
1005-
writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
987+
writer.WriteProperty(OpenApiConstants.Type, typeToUse.Value.ToFirstIdentifier());
1006988
return true;
1007989
}
1008990
break;
@@ -1014,30 +996,6 @@ private bool TrySerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion
1014996
return false;
1015997
}
1016998

1017-
private JsonNode? GetCompatibilityExample()
1018-
{
1019-
#pragma warning disable CS0618 // Type or member is obsolete
1020-
return Example ?? Examples?.FirstOrDefault();
1021-
#pragma warning restore CS0618 // Type or member is obsolete
1022-
}
1023-
1024-
private IEnumerable<JsonNode>? GetCompatibilityExamplesExtension()
1025-
{
1026-
if (Examples is null || Examples.Count == 0)
1027-
{
1028-
return null;
1029-
}
1030-
1031-
#pragma warning disable CS0618 // Type or member is obsolete
1032-
if (Example is not null)
1033-
{
1034-
return Examples;
1035-
}
1036-
#pragma warning restore CS0618 // Type or member is obsolete
1037-
1038-
return Examples.Count > 1 ? Examples.Skip(1) : null;
1039-
}
1040-
1041999
private static bool IsPowerOfTwo(int x)
10421000
{
10431001
return x != 0 && (x & (x - 1)) == 0;
@@ -1072,7 +1030,7 @@ where type.HasFlag(flag)
10721030

10731031
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
10741032
{
1075-
if (HasNullType)
1033+
if (IsNullableCompatibility)
10761034
{
10771035
switch (version)
10781036
{
@@ -1086,6 +1044,62 @@ private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version
10861044
}
10871045
}
10881046

1047+
internal void ConvertNullTypeToNullableCompatibility()
1048+
{
1049+
if (Type is not { } type || !type.HasFlag(JsonSchemaType.Null))
1050+
{
1051+
return;
1052+
}
1053+
1054+
var typeWithoutNull = type & ~JsonSchemaType.Null;
1055+
if (typeWithoutNull != 0)
1056+
{
1057+
Type = typeWithoutNull;
1058+
IsNullableCompatibility = true;
1059+
}
1060+
}
1061+
1062+
internal void ConvertNullableCompatibilityToNullType()
1063+
{
1064+
if (IsNullableCompatibility && Type is not null and not 0)
1065+
{
1066+
Type |= JsonSchemaType.Null;
1067+
IsNullableCompatibility = false;
1068+
}
1069+
}
1070+
1071+
internal void ConvertExclusiveBoundsToCompatibilityBooleans()
1072+
{
1073+
if (!string.IsNullOrEmpty(_exclusiveMaximum))
1074+
{
1075+
_maximum = _exclusiveMaximum;
1076+
_exclusiveMaximum = null;
1077+
IsExclusiveMaximum = true;
1078+
}
1079+
1080+
if (!string.IsNullOrEmpty(_exclusiveMinimum))
1081+
{
1082+
_minimum = _exclusiveMinimum;
1083+
_exclusiveMinimum = null;
1084+
IsExclusiveMinimum = true;
1085+
}
1086+
}
1087+
1088+
internal void ConvertCompatibilityBooleansToExclusiveBounds()
1089+
{
1090+
if (IsExclusiveMaximum == true && !string.IsNullOrEmpty(_maximum))
1091+
{
1092+
_exclusiveMaximum = _maximum;
1093+
_maximum = null;
1094+
}
1095+
1096+
if (IsExclusiveMinimum == true && !string.IsNullOrEmpty(_minimum))
1097+
{
1098+
_exclusiveMinimum = _minimum;
1099+
_minimum = null;
1100+
}
1101+
}
1102+
10891103
#if NET5_0_OR_GREATER
10901104
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues<JsonSchemaType>();
10911105
#else

src/Microsoft.OpenApi/PublicAPI.Unshipped.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ const Microsoft.OpenApi.OpenApiConstants.OaiLicenseIdentifier = "x-oai-license-i
1313
[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter.Convert(Microsoft.OpenApi.OpenApiDocument! document) -> void
1414
[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter.OpenApiV3ToV3_1DeserializationConverter() -> void
1515
[OPENAPI001]override Microsoft.OpenApi.OpenApiV3ToV3_1DeserializationConverter.Visit(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
16+
[OPENAPI001]Microsoft.OpenApi.OpenApiV2ToV3DeserializationConverter
17+
[OPENAPI001]Microsoft.OpenApi.OpenApiV2ToV3DeserializationConverter.Convert(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
18+
[OPENAPI001]Microsoft.OpenApi.OpenApiV2ToV3DeserializationConverter.Convert(Microsoft.OpenApi.OpenApiDocument! document) -> void
19+
[OPENAPI001]Microsoft.OpenApi.OpenApiV2ToV3DeserializationConverter.OpenApiV2ToV3DeserializationConverter() -> void
20+
[OPENAPI001]override Microsoft.OpenApi.OpenApiV2ToV3DeserializationConverter.Visit(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
21+
[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV2SerializationConverter
22+
[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV2SerializationConverter.Convert(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
23+
[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV2SerializationConverter.Convert(Microsoft.OpenApi.OpenApiDocument! document) -> void
24+
[OPENAPI001]Microsoft.OpenApi.OpenApiV3ToV2SerializationConverter.OpenApiV3ToV2SerializationConverter() -> void
25+
[OPENAPI001]override Microsoft.OpenApi.OpenApiV3ToV2SerializationConverter.Visit(Microsoft.OpenApi.IOpenApiSchema! schema) -> void
1626
[OPENAPI001]Microsoft.OpenApi.OpenApiCallback.CreateDeepCopy() -> Microsoft.OpenApi.IOpenApiCallback!
1727
[OPENAPI001]Microsoft.OpenApi.OpenApiCallbackReference.CreateDeepCopy() -> Microsoft.OpenApi.IOpenApiCallback!
1828
[OPENAPI001]Microsoft.OpenApi.OpenApiComponents.CreateDeepCopy() -> Microsoft.OpenApi.OpenApiComponents!

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ internal static partial class OpenApiV2Deserializer
222222
{
223223
if (o.Type is not null && o.Type != 0)
224224
{
225-
o.Type |= JsonSchemaType.Null;
225+
o.IsNullableCompatibility = true;
226226
}
227227
else
228228
{
@@ -304,7 +304,7 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
304304
schema.Metadata.Remove(EncounteredNullableExtensionTrueMetadataKey);
305305
if (schema.Type is not null && schema.Type != 0 && value is bool isNullable && isNullable)
306306
{
307-
schema.Type |= JsonSchemaType.Null;
307+
schema.IsNullableCompatibility = true;
308308
}
309309
}
310310

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ internal static partial class OpenApiV3Deserializer
229229
{
230230
if (o.Type is not null && o.Type != 0)
231231
{
232-
o.Type |= JsonSchemaType.Null;
232+
o.IsNullableCompatibility = true;
233233
}
234234
else
235235
{
@@ -409,7 +409,7 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
409409
schema.Metadata.Remove(EncounteredNullableTrueMetadataKey);
410410
if (schema.Type is not null && schema.Type != 0 && value is bool isNullable && isNullable)
411411
{
412-
schema.Type |= JsonSchemaType.Null;
412+
schema.IsNullableCompatibility = true;
413413
}
414414
}
415415

0 commit comments

Comments
 (0)