From c532c4ea6f8788afbfd518bc1a849e4cbc1480d0 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Fri, 12 Dec 2025 22:31:54 +0100 Subject: [PATCH 01/10] Add type: null downcasting with oneOf and anyOf for OpenApi v3 --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 87 ++++- .../Models/OpenApiSchemaTests.cs | 312 ++++++++++++++++++ 2 files changed, 387 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index f31644c1f..140b9f9e3 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -438,23 +438,39 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version // enum var enumValue = Enum is not { Count: > 0 } - && !string.IsNullOrEmpty(Const) + && !string.IsNullOrEmpty(Const) && version < OpenApiSpecVersion.OpenApi3_1 ? new List { JsonValue.Create(Const)! } : Enum; writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s)); + // Handle oneOf/anyOf with null type for v3.0 downcast + IList? effectiveOneOf = OneOf; + IList? effectiveAnyOf = AnyOf; + bool hasNullInComposition = false; + JsonSchemaType? inferredType = null; + + if (version == OpenApiSpecVersion.OpenApi3_0) + { + (effectiveOneOf, var inferredOneOf, var nullInOneOf) = ProcessCompositionForNull(OneOf); + hasNullInComposition |= nullInOneOf; + inferredType = inferredOneOf ?? inferredType; + (effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf); + hasNullInComposition |= nullInAnyOf; + inferredType = inferredAnyOf ?? inferredType; + } + // type - SerializeTypeProperty(writer, version); + SerializeTypeProperty(writer, version, inferredType); // allOf writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback); // anyOf - writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback); + writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback); // oneOf - writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback); + writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback); // not writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback); @@ -493,7 +509,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version // nullable if (version == OpenApiSpecVersion.OpenApi3_0) { - SerializeNullable(writer, version); + SerializeNullable(writer, version, hasNullInComposition); } // discriminator @@ -766,14 +782,17 @@ private void SerializeAsV2( writer.WriteEndObject(); } - private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version) + private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null) { - if (Type is null) + // Use inferred type from oneOf/anyOf if provided and original type is not set + var typeToUse = inferredType ?? Type; + + if (typeToUse is null) { return; } - var unifiedType = IsNullable ? Type.Value | JsonSchemaType.Null : Type.Value; + var unifiedType = IsNullable ? typeToUse.Value | JsonSchemaType.Null : typeToUse.Value; var typeWithoutNull = unifiedType & ~JsonSchemaType.Null; switch (version) @@ -804,8 +823,8 @@ private static bool HasMultipleTypes(JsonSchemaType schemaType) private static void WriteUnifiedSchemaType(JsonSchemaType type, IOpenApiWriter writer) { var array = (from JsonSchemaType flag in jsonSchemaTypeValues - where type.HasFlag(flag) - select flag.ToFirstIdentifier()).ToArray(); + where type.HasFlag(flag) + select flag.ToFirstIdentifier()).ToArray(); if (array.Length > 1) { writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) => @@ -822,9 +841,9 @@ where type.HasFlag(flag) } } - private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version) + private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version, bool hasNullInComposition = false) { - if (IsNullable) + if (IsNullable || hasNullInComposition) { switch (version) { @@ -838,6 +857,50 @@ private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version } } + /// + /// Processes a composition (oneOf or anyOf) for null types, filtering out null schemas and inferring common type. + /// + /// The list of schemas in the composition. + /// A tuple with the effective list, inferred type, and whether null is present in composition. + private static (IList? effective, JsonSchemaType? inferredType, bool hasNullInComposition) + ProcessCompositionForNull(IList? composition) + { + if (composition is null || !composition.Any(s => s.Type is JsonSchemaType.Null)) + { + // Nothing to patch + return (composition, null, false); + } + + var nonNullSchemas = composition + .Where(s => s.Type is null or not JsonSchemaType.Null) + .ToList(); + + if (nonNullSchemas.Count > 0) + { + JsonSchemaType commonType = 0; + + foreach (var schema in nonNullSchemas) + { + commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null; + } + + if (System.Enum.IsDefined(commonType)) + { + // Single common type + return (nonNullSchemas, commonType, true); + } + else + { + return (nonNullSchemas, null, true); + } + + } + else + { + return (null, null, true); + } + } + #if NET5_0_OR_GREATER private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues(); #else diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 6ffbb9fe4..7f4fc4e9a 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -790,6 +790,318 @@ public async Task SerializeAdditionalPropertiesAsV3PlusEmits(OpenApiSpecVersion Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual))); } + [Fact] + public async Task SerializeOneOfWithNullAsV3ShouldUseNullableAsync() + { + // Arrange - oneOf with null and a reference-like schema + var schema = new OpenApiSchema + { + OneOf = new List + { + new OpenApiSchema { Type = JsonSchemaType.Null }, + new OpenApiSchema + { + Type = JsonSchemaType.String, + MaxLength = 10 + } + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV3(writer); + await writer.FlushAsync(); + + var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV3Schema = + """ + { + "type": "string", + "oneOf": [ + { + "maxLength": 10, + "type": "string" + } + ], + "nullable": true + } + """.MakeLineBreaksEnvironmentNeutral(); + + // Assert + Assert.Equal(expectedV3Schema, v3Schema); + } + + [Fact] + public async Task SerializeOneOfWithNullAndMultipleSchemasAsV3ShouldMarkItAsNullableWithoutType() + { + // Arrange - oneOf with null, string, and number + var schema = new OpenApiSchema + { + OneOf = new List + { + new OpenApiSchema { Type = JsonSchemaType.Null }, + new OpenApiSchema { Type = JsonSchemaType.String }, + new OpenApiSchema { Type = JsonSchemaType.Number } + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV3(writer); + await writer.FlushAsync(); + + var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV3Schema = + """ + { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "nullable": true + } + """.MakeLineBreaksEnvironmentNeutral(); + + // Assert + Assert.Equal(expectedV3Schema, v3Schema); + } + + [Fact] + public async Task SerializeAnyOfWithNullAsV3ShouldUseNullableAsync() + { + // Arrange - anyOf with null and object schema + 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 outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV3(writer); + await writer.FlushAsync(); + + var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV3Schema = + """ + { + "type": "object", + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + } + ], + "nullable": true + } + """.MakeLineBreaksEnvironmentNeutral(); // Assert + Assert.Equal(expectedV3Schema, v3Schema); + } + + [Fact] + public async Task SerializeAnyOfWithNullAndMultipleSchemasAsV3ShouldApplyNullable() + { + // Arrange - anyOf with null and multiple schemas + 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 outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV3(writer); + await writer.FlushAsync(); + + var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV3Schema = + """ + { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "integer" + } + ], + "nullable": true + } + """.MakeLineBreaksEnvironmentNeutral(); + + // Assert + Assert.Equal(expectedV3Schema, v3Schema); + } + + [Fact] + public async Task SerializeOneOfWithOnlyNullAsV3ShouldJustBeNullableAsync() + { + // Arrange - oneOf with only null + var schema = new OpenApiSchema + { + OneOf = new List + { + new OpenApiSchema { Type = JsonSchemaType.Null } + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV3(writer); + await writer.FlushAsync(); + + var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV3Schema = + """ + { + "nullable": true + } + """.MakeLineBreaksEnvironmentNeutral(); + + // Assert + Assert.Equal(expectedV3Schema, v3Schema); + } + + [Fact] + public async Task SerializeOneOfWithNullAsV31ShouldNotChangeAsync() + { + // Arrange - oneOf with null should remain unchanged in v3.1 + var schema = new OpenApiSchema + { + OneOf = new List + { + new OpenApiSchema { Type = JsonSchemaType.Null }, + new OpenApiSchema { Type = JsonSchemaType.String } + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV31(writer); + await writer.FlushAsync(); + + var v31Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV31Schema = + """ + { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string" + } + ] + } + """.MakeLineBreaksEnvironmentNeutral(); + + // Assert + Assert.Equal(expectedV31Schema, v31Schema); + } + + [Fact] + public async Task SerializeOneOfWithNullAndRefAsV3ShouldUseNullableAsync() + { + // Arrange - oneOf with null and a $ref to a schema component + 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 } + } + } + } + } + }; + + // Register components so references can be resolved + document.Workspace.RegisterComponents(document); + + var schemaRef = new OpenApiSchemaReference("Pet", document); + + var schema = new OpenApiSchema + { + OneOf = new List + { + new OpenApiSchema { Type = JsonSchemaType.Null }, + schemaRef + } + }; + + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new() { Terse = false }); + + // Act + schema.SerializeAsV3(writer); + await writer.FlushAsync(); + + var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + + var expectedV3Schema = + """ + { + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/Pet" + } + ], + "nullable": true + } + """.MakeLineBreaksEnvironmentNeutral(); + + // Assert + Assert.Equal(expectedV3Schema, v3Schema); + } internal class SchemaVisitor : OpenApiVisitorBase { From f72d77c9640c0eef1812787490bcc482cb081639 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Mon, 15 Dec 2025 20:40:32 +0100 Subject: [PATCH 02/10] Apply suggestions from code review Co-authored-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 140b9f9e3..c3836d22f 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -865,14 +865,14 @@ private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version private static (IList? effective, JsonSchemaType? inferredType, bool hasNullInComposition) ProcessCompositionForNull(IList? composition) { - if (composition is null || !composition.Any(s => s.Type is JsonSchemaType.Null)) + if (composition is null || !composition.Any(static s => s.Type is JsonSchemaType.Null)) { // Nothing to patch return (composition, null, false); } var nonNullSchemas = composition - .Where(s => s.Type is null or not JsonSchemaType.Null) + .Where(static s => s.Type is null or not JsonSchemaType.Null) .ToList(); if (nonNullSchemas.Count > 0) From f9984c90e5a19dea6ee32daef2b73a8e08ebe232 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:42:35 +0000 Subject: [PATCH 03/10] Initial plan From cf00a1e0ff20e7b9dd059b79c13e3a7e3b30b275 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:49:16 +0000 Subject: [PATCH 04/10] Fix PR review comments: replace Enum.IsDefined with power-of-2 check and use JsonNode.DeepEquals in tests Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 4 +- .../Models/OpenApiSchemaTests.cs | 43 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index c3836d22f..45f94f011 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -884,7 +884,9 @@ private static (IList? effective, JsonSchemaType? inferredType, commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null; } - if (System.Enum.IsDefined(commonType)) + // Check if commonType is a single flag (power of 2) indicating all schemas share the same type + var isSingleType = commonType != 0 && (commonType & (commonType - 1)) == 0; + if (isSingleType) { // Single common type return (nonNullSchemas, commonType, true); diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 7f4fc4e9a..d4eb0aeab 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -814,7 +814,7 @@ public async Task SerializeOneOfWithNullAsV3ShouldUseNullableAsync() schema.SerializeAsV3(writer); await writer.FlushAsync(); - var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + var v3Schema = outputStringWriter.GetStringBuilder().ToString(); var expectedV3Schema = """ @@ -828,10 +828,10 @@ public async Task SerializeOneOfWithNullAsV3ShouldUseNullableAsync() ], "nullable": true } - """.MakeLineBreaksEnvironmentNeutral(); + """; // Assert - Assert.Equal(expectedV3Schema, v3Schema); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); } [Fact] @@ -855,7 +855,7 @@ public async Task SerializeOneOfWithNullAndMultipleSchemasAsV3ShouldMarkItAsNull schema.SerializeAsV3(writer); await writer.FlushAsync(); - var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + var v3Schema = outputStringWriter.GetStringBuilder().ToString(); var expectedV3Schema = """ @@ -870,10 +870,10 @@ public async Task SerializeOneOfWithNullAndMultipleSchemasAsV3ShouldMarkItAsNull ], "nullable": true } - """.MakeLineBreaksEnvironmentNeutral(); + """; // Assert - Assert.Equal(expectedV3Schema, v3Schema); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); } [Fact] @@ -903,7 +903,7 @@ public async Task SerializeAnyOfWithNullAsV3ShouldUseNullableAsync() schema.SerializeAsV3(writer); await writer.FlushAsync(); - var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + var v3Schema = outputStringWriter.GetStringBuilder().ToString(); var expectedV3Schema = """ @@ -921,8 +921,9 @@ public async Task SerializeAnyOfWithNullAsV3ShouldUseNullableAsync() ], "nullable": true } - """.MakeLineBreaksEnvironmentNeutral(); // Assert - Assert.Equal(expectedV3Schema, v3Schema); + """; + // Assert + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); } [Fact] @@ -946,7 +947,7 @@ public async Task SerializeAnyOfWithNullAndMultipleSchemasAsV3ShouldApplyNullabl schema.SerializeAsV3(writer); await writer.FlushAsync(); - var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + var v3Schema = outputStringWriter.GetStringBuilder().ToString(); var expectedV3Schema = """ @@ -962,10 +963,10 @@ public async Task SerializeAnyOfWithNullAndMultipleSchemasAsV3ShouldApplyNullabl ], "nullable": true } - """.MakeLineBreaksEnvironmentNeutral(); + """; // Assert - Assert.Equal(expectedV3Schema, v3Schema); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); } [Fact] @@ -987,17 +988,17 @@ public async Task SerializeOneOfWithOnlyNullAsV3ShouldJustBeNullableAsync() schema.SerializeAsV3(writer); await writer.FlushAsync(); - var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + var v3Schema = outputStringWriter.GetStringBuilder().ToString(); var expectedV3Schema = """ { "nullable": true } - """.MakeLineBreaksEnvironmentNeutral(); + """; // Assert - Assert.Equal(expectedV3Schema, v3Schema); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); } [Fact] @@ -1020,7 +1021,7 @@ public async Task SerializeOneOfWithNullAsV31ShouldNotChangeAsync() schema.SerializeAsV31(writer); await writer.FlushAsync(); - var v31Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + var v31Schema = outputStringWriter.GetStringBuilder().ToString(); var expectedV31Schema = """ @@ -1034,10 +1035,10 @@ public async Task SerializeOneOfWithNullAsV31ShouldNotChangeAsync() } ] } - """.MakeLineBreaksEnvironmentNeutral(); + """; // Assert - Assert.Equal(expectedV31Schema, v31Schema); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV31Schema), JsonNode.Parse(v31Schema))); } [Fact] @@ -1084,7 +1085,7 @@ public async Task SerializeOneOfWithNullAndRefAsV3ShouldUseNullableAsync() schema.SerializeAsV3(writer); await writer.FlushAsync(); - var v3Schema = outputStringWriter.GetStringBuilder().ToString().MakeLineBreaksEnvironmentNeutral(); + var v3Schema = outputStringWriter.GetStringBuilder().ToString(); var expectedV3Schema = """ @@ -1097,10 +1098,10 @@ public async Task SerializeOneOfWithNullAndRefAsV3ShouldUseNullableAsync() ], "nullable": true } - """.MakeLineBreaksEnvironmentNeutral(); + """; // Assert - Assert.Equal(expectedV3Schema, v3Schema); + Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expectedV3Schema), JsonNode.Parse(v3Schema))); } internal class SchemaVisitor : OpenApiVisitorBase From fadc853da8aac29e9bd734b7398ec3561db44817 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 22:58:51 +0000 Subject: [PATCH 05/10] Rewrite type checking logic to avoid cryptic binary operators Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 45f94f011..9ba6079d0 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -877,22 +877,35 @@ private static (IList? effective, JsonSchemaType? inferredType, if (nonNullSchemas.Count > 0) { - JsonSchemaType commonType = 0; + // Check if all schemas have the same type + JsonSchemaType? firstType = null; + bool allSameType = true; foreach (var schema in nonNullSchemas) { - commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null; + var schemaType = schema.Type; + if (schemaType.HasValue && schemaType.Value != JsonSchemaType.Null) + { + if (firstType == null) + { + firstType = schemaType.Value; + } + else if (firstType != schemaType.Value) + { + allSameType = false; + break; + } + } } - // Check if commonType is a single flag (power of 2) indicating all schemas share the same type - var isSingleType = commonType != 0 && (commonType & (commonType - 1)) == 0; - if (isSingleType) + if (allSameType && firstType.HasValue) { - // Single common type - return (nonNullSchemas, commonType, true); + // All schemas share the same type + return (nonNullSchemas, firstType.Value, true); } else { + // Multiple different types return (nonNullSchemas, null, true); } From 3f2402f28698bc9b983803706544d1bc6351d89c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:37:37 +0000 Subject: [PATCH 06/10] Use bitwise operator to remove Null flag from type comparison Co-authored-by: desjoerd <2460430+desjoerd@users.noreply.github.com> --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 9ba6079d0..36f10bd1d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -877,23 +877,29 @@ private static (IList? effective, JsonSchemaType? inferredType, if (nonNullSchemas.Count > 0) { - // Check if all schemas have the same type + // Check if all schemas have the same type (excluding null) JsonSchemaType? firstType = null; bool allSameType = true; foreach (var schema in nonNullSchemas) { var schemaType = schema.Type; - if (schemaType.HasValue && schemaType.Value != JsonSchemaType.Null) + if (schemaType.HasValue) { - if (firstType == null) + // Remove null from the type using bitwise operator + var typeWithoutNull = schemaType.Value & ~JsonSchemaType.Null; + + if (typeWithoutNull != 0) { - firstType = schemaType.Value; - } - else if (firstType != schemaType.Value) - { - allSameType = false; - break; + if (firstType == null) + { + firstType = typeWithoutNull; + } + else if (firstType != typeWithoutNull) + { + allSameType = false; + break; + } } } } From 59240b3e040a6d50f8976ac859305901200ce708 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Tue, 16 Dec 2025 10:30:52 +0100 Subject: [PATCH 07/10] Prefer the original type before the inferred type --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 140b9f9e3..8eef1ae0d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -784,8 +784,8 @@ private void SerializeAsV2( private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null) { - // Use inferred type from oneOf/anyOf if provided and original type is not set - var typeToUse = inferredType ?? Type; + // Use original type or inferred type when the explicit type is not set + var typeToUse = Type ?? inferredType; if (typeToUse is null) { From 9a2ee7e396da3f9ec2636ef678ae2a7ffdc626d2 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Tue, 16 Dec 2025 10:34:10 +0100 Subject: [PATCH 08/10] Remove useless comment --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index d7a5b0cef..1f492fa2d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -886,7 +886,6 @@ private static (IList? effective, JsonSchemaType? inferredType, var schemaType = schema.Type; if (schemaType.HasValue) { - // Remove null from the type using bitwise operator var typeWithoutNull = schemaType.Value & ~JsonSchemaType.Null; if (typeWithoutNull != 0) From 263e3ca921d471274328f6ff402deead991d1140 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Tue, 16 Dec 2025 10:42:33 +0100 Subject: [PATCH 09/10] Use HasMultipleTypes helper method --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 1f492fa2d..642a5c58e 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -877,43 +877,24 @@ private static (IList? effective, JsonSchemaType? inferredType, if (nonNullSchemas.Count > 0) { - // Check if all schemas have the same type (excluding null) - JsonSchemaType? firstType = null; - bool allSameType = true; + JsonSchemaType commonType = 0; foreach (var schema in nonNullSchemas) { - var schemaType = schema.Type; - if (schemaType.HasValue) - { - var typeWithoutNull = schemaType.Value & ~JsonSchemaType.Null; - - if (typeWithoutNull != 0) - { - if (firstType == null) - { - firstType = typeWithoutNull; - } - else if (firstType != typeWithoutNull) - { - allSameType = false; - break; - } - } - } + commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null; } - if (allSameType && firstType.HasValue) + var isSingleType = !HasMultipleTypes(commonType); + if (isSingleType) { - // All schemas share the same type - return (nonNullSchemas, firstType.Value, true); + // Single common type + return (nonNullSchemas, commonType, true); } else { // Multiple different types return (nonNullSchemas, null, true); } - } else { From 3673612ae47a8e375127748bb1d818bab91944b8 Mon Sep 17 00:00:00 2001 From: Sjoerd van der Meer Date: Tue, 16 Dec 2025 10:44:36 +0100 Subject: [PATCH 10/10] Improve readability of ProcessCompositionForNull --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 642a5c58e..043a9d183 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -884,16 +884,14 @@ private static (IList? effective, JsonSchemaType? inferredType, commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null; } - var isSingleType = !HasMultipleTypes(commonType); - if (isSingleType) + if (HasMultipleTypes(commonType) || commonType == 0) { - // Single common type - return (nonNullSchemas, commonType, true); + return (nonNullSchemas, null, true); } else { - // Multiple different types - return (nonNullSchemas, null, true); + // Single common type + return (nonNullSchemas, commonType, true); } } else