From fe0b50aae372e5b08625f888ae424f22da759cdf Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 07:57:13 -0400 Subject: [PATCH 1/9] fix: null reference exception for boolean component schemas Co-authored-by: Copilot Signed-off-by: Vincent Biret --- .../Services/OpenApiWorkspace.cs | 11 +++++++++++ .../V31Tests/OpenApiDocumentTests.cs | 18 ++++++++++++++++++ .../V32Tests/OpenApiDocumentTests.cs | 18 ++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 14c8d2ee4..22128adc2 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -93,6 +93,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.Schemas) { + if (item.Value == null) continue; location = item.Value.Id ?? baseUri + ReferenceType.Schema.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -103,6 +104,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.Parameters) { + if (item.Value == null) continue; location = baseUri + ReferenceType.Parameter.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -113,6 +115,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.Responses) { + if (item.Value == null) continue; location = baseUri + ReferenceType.Response.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -123,6 +126,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.RequestBodies) { + if (item.Value == null) continue; location = baseUri + ReferenceType.RequestBody.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -133,6 +137,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.Links) { + if (item.Value == null) continue; location = baseUri + ReferenceType.Link.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -143,6 +148,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.Callbacks) { + if (item.Value == null) continue; location = baseUri + ReferenceType.Callback.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -153,6 +159,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.PathItems) { + if (item.Value == null) continue; location = baseUri + ReferenceType.PathItem.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -163,6 +170,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.Examples) { + if (item.Value == null) continue; location = baseUri + ReferenceType.Example.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -173,6 +181,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.Headers) { + if (item.Value == null) continue; location = baseUri + ReferenceType.Header.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -183,6 +192,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.SecuritySchemes) { + if (item.Value == null) continue; location = baseUri + ReferenceType.SecurityScheme.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } @@ -193,6 +203,7 @@ public void RegisterComponents(OpenApiDocument document) { foreach (var item in document.Components.MediaTypes) { + if (item.Value == null) continue; location = baseUri + ReferenceType.MediaType.GetDisplayName() + ComponentSegmentSeparator + item.Key; RegisterComponent(location, item.Value); } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs index 28085736c..8acf84d09 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs @@ -630,5 +630,23 @@ public async Task ParseDocumentWithSelfExtensionWorks() Assert.Empty(result.Diagnostic.Errors); Assert.Empty(result.Diagnostic.Warnings); } + + [Fact] + public void LoadDocumentWithBooleanSchemaShouldNotThrowNullReferenceException() + { + // Arrange - OpenAPI 3.1 with a boolean schema in components/schemas (spec-valid per JSON Schema 2020-12) + var bytes = "{\"openapi\":\"3.1.0\",\"components\":{\"schemas\":{\"X\":true}}}"u8.ToArray(); + using var ms = new MemoryStream(bytes); + + // Act & Assert - should not throw NullReferenceException + var exception = Record.Exception(() => OpenApiDocument.Load(ms, format: null, new OpenApiReaderSettings())); + + // The parser should handle the boolean schema gracefully + // Either accepting it or surfacing a structured diagnostic, but not throwing NullReferenceException + if (exception != null) + { + Assert.IsNotType(exception); + } + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs index ed868a913..80051f05c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiDocumentTests.cs @@ -645,6 +645,24 @@ public async Task ParseDocumentWithSelfPropertyFromJsonWorks() Assert.Empty(result.Diagnostic.Errors); Assert.Empty(result.Diagnostic.Warnings); } + + [Fact] + public void LoadDocumentWithBooleanSchemaShouldNotThrowNullReferenceException() + { + // Arrange - OpenAPI 3.2 with a boolean schema in components/schemas (spec-valid per JSON Schema 2020-12) + var bytes = "{\"openapi\":\"3.2.0\",\"components\":{\"schemas\":{\"X\":true}}}"u8.ToArray(); + using var ms = new MemoryStream(bytes); + + // Act & Assert - should not throw NullReferenceException + var exception = Record.Exception(() => OpenApiDocument.Load(ms, format: null, new OpenApiReaderSettings())); + + // The parser should handle the boolean schema gracefully + // Either accepting it or surfacing a structured diagnostic, but not throwing NullReferenceException + if (exception != null) + { + Assert.IsNotType(exception); + } + } } } From b6c1fe83d8e707ebce71cd74f999b491af1f7e02 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:00:55 -0400 Subject: [PATCH 2/9] test(schema): validate empty schema serialization across all OpenAPI versions Convert SerializeBasicSchemaAsV3JsonWorks from a single-version test to a theory that validates empty schema serialization for all supported versions (2.0, 3.0, 3.1, 3.2). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/OpenApiSchemaTests.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 64874fb37..40034eded 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -204,14 +204,18 @@ public class OpenApiSchemaTests } }; - [Fact] - public async Task SerializeBasicSchemaAsV3JsonWorks() + [Theory] + [InlineData(OpenApiSpecVersion.OpenApi2_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_0)] + [InlineData(OpenApiSpecVersion.OpenApi3_1)] + [InlineData(OpenApiSpecVersion.OpenApi3_2)] + public async Task SerializeBasicSchemaAsJsonWorks(OpenApiSpecVersion version) { // Arrange var expected = @"{ }"; // Act - var actual = await BasicSchema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0); + var actual = await BasicSchema.SerializeAsJsonAsync(version); // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); From b9b9b7584a2dba829935c27f24df0b7fe5acd96b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:06:21 -0400 Subject: [PATCH 3/9] test(schema): add deserialization tests for empty and boolean schemas Add theory tests across OpenAPI 3.1 and 3.2 to validate deserialization of: - Empty schema object ({}) - Boolean schema value (true) These tests ensure that non-standard but valid schema forms are parsed without errors and produce valid OpenApiSchema objects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../V31Tests/OpenApiSchemaTests.cs | 12 ++++++++++++ .../V32Tests/OpenApiSchemaTests.cs | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index 7ed518b95..81ace6bb9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -844,5 +844,17 @@ public void ParseSchemaWithoutUnevaluatedPropertiesDefaultsToTrue() Assert.Equivalent(expected, actual); Assert.True(actual.UnevaluatedProperties); // Explicitly verify the default } + + [Theory] + [InlineData("{}")] + [InlineData("true")] + public void DeserializeBasicSchemaWorks(string schemaSource) + { + // Arrange & Act + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _); + + // Assert - schema should deserialize without error + Assert.NotNull(schema); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs index 286a0baf8..edc3b5bb1 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs @@ -695,6 +695,18 @@ public void ParseSchemaWithUnevaluatedPropertiesComplexSchema() // Assert Assert.Equivalent(expected, actual); } + + [Theory] + [InlineData("{}")] + [InlineData("true")] + public void DeserializeBasicSchemaWorks(string schemaSource) + { + // Arrange & Act + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _); + + // Assert - schema should deserialize without error + Assert.NotNull(schema); + } } } From 7d43b15b29ace29bfe344576d79b4bd4d43f144e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:09:31 -0400 Subject: [PATCH 4/9] test(schema): validate false schema deserializes to not with empty object Add tests for both OpenAPI 3.1 and 3.2 that verify when a schema is simply 'false', it deserializes to a schema with a Not property containing an empty schema object. This ensures JSON Schema 2020-12 boolean schema semantics are properly handled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../V31Tests/OpenApiSchemaTests.cs | 17 +++++++++++++++++ .../V32Tests/OpenApiSchemaTests.cs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index 81ace6bb9..5e248a756 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -856,5 +856,22 @@ public void DeserializeBasicSchemaWorks(string schemaSource) // Assert - schema should deserialize without error Assert.NotNull(schema); } + + [Fact] + public void DeserializeFalseSchemaParsesAsNotEmptySchema() + { + // Arrange + var schemaSource = "false"; + + // Act + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _); + + // Assert - false schema should deserialize to not: {} + Assert.NotNull(schema); + Assert.NotNull(schema.Not); + Assert.Empty(schema.Not.AnyOf ?? []); + Assert.Empty(schema.Not.AllOf ?? []); + Assert.Empty(schema.Not.OneOf ?? []); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs index edc3b5bb1..c8674c774 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs @@ -707,6 +707,23 @@ public void DeserializeBasicSchemaWorks(string schemaSource) // Assert - schema should deserialize without error Assert.NotNull(schema); } + + [Fact] + public void DeserializeFalseSchemaParsesAsNotEmptySchema() + { + // Arrange + var schemaSource = "false"; + + // Act + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _); + + // Assert - false schema should deserialize to not: {} + Assert.NotNull(schema); + Assert.NotNull(schema.Not); + Assert.Empty(schema.Not.AnyOf ?? []); + Assert.Empty(schema.Not.AllOf ?? []); + Assert.Empty(schema.Not.OneOf ?? []); + } } } From 15c1305d3e2c0603ae4e68bd495b76f0c0ff0b98 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:12:02 -0400 Subject: [PATCH 5/9] tests: pass serialization format to disambiguate Co-authored-by: Copilot Signed-off-by: Vincent Biret --- .../V31Tests/OpenApiSchemaTests.cs | 4 ++-- .../V32Tests/OpenApiSchemaTests.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index 5e248a756..e743732f9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -851,7 +851,7 @@ public void ParseSchemaWithoutUnevaluatedPropertiesDefaultsToTrue() public void DeserializeBasicSchemaWorks(string schemaSource) { // Arrange & Act - var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _); + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _, OpenApiConstants.Json); // Assert - schema should deserialize without error Assert.NotNull(schema); @@ -864,7 +864,7 @@ public void DeserializeFalseSchemaParsesAsNotEmptySchema() var schemaSource = "false"; // Act - var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _); + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _, OpenApiConstants.Json); // Assert - false schema should deserialize to not: {} Assert.NotNull(schema); diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs index c8674c774..d2a4d8569 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.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.Collections.Generic; @@ -702,7 +702,7 @@ public void ParseSchemaWithUnevaluatedPropertiesComplexSchema() public void DeserializeBasicSchemaWorks(string schemaSource) { // Arrange & Act - var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _); + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _, OpenApiConstants.Json); // Assert - schema should deserialize without error Assert.NotNull(schema); @@ -715,7 +715,7 @@ public void DeserializeFalseSchemaParsesAsNotEmptySchema() var schemaSource = "false"; // Act - var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _); + var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _, OpenApiConstants.Json); // Assert - false schema should deserialize to not: {} Assert.NotNull(schema); From 05b44beb9783f5bab1d8988b3cb916d0278d42ce Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:14:50 -0400 Subject: [PATCH 6/9] fix(schema): support boolean schemas in deserializer for OpenAPI 3.1/3.2 Implement special handling for JSON Schema 2020-12 boolean schemas: - true: deserializes to empty schema (allows any value) - false: deserializes to schema with not: {} (disallows any value) This enables proper support for boolean schemas as defined in the OpenAPI 3.1 and 3.2 specifications, which adopt JSON Schema 2020-12. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Reader/V31/OpenApiSchemaDeserializer.cs | 17 +++++++++++++++++ .../Reader/V32/OpenApiSchemaDeserializer.cs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index ee1ca1832..65da0aa59 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -362,6 +362,23 @@ internal static partial class OpenApiV31Deserializer public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocument) { + // Handle boolean schemas (true/false) for JSON Schema 2020-12 compatibility + if (node is ValueNode valueNode) + { + var value = valueNode.GetScalarValue(); + if (value != null && bool.TryParse(value, out var boolValue)) + { + var boolSchema = new OpenApiSchema(); + if (!boolValue) + { + // false schema: represents "not valid" -> convert to "not: {}" + boolSchema.Not = new OpenApiSchema(); + } + // true schema: represents "always valid" -> return empty schema (default) + return boolSchema; + } + } + var mapNode = node.CheckMapNode(OpenApiConstants.Schema); var pointer = mapNode.GetReferencePointer(); diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs index 5dd32b413..b880806e9 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs @@ -362,6 +362,23 @@ internal static partial class OpenApiV32Deserializer public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocument) { + // Handle boolean schemas (true/false) for JSON Schema 2020-12 compatibility + if (node is ValueNode valueNode) + { + var value = valueNode.GetScalarValue(); + if (value != null && bool.TryParse(value, out var boolValue)) + { + var boolSchema = new OpenApiSchema(); + if (!boolValue) + { + // false schema: represents "not valid" -> convert to "not: {}" + boolSchema.Not = new OpenApiSchema(); + } + // true schema: represents "always valid" -> return empty schema (default) + return boolSchema; + } + } + var mapNode = node.CheckMapNode(OpenApiConstants.Schema); var pointer = mapNode.GetReferencePointer(); From 509f3327975552b974962ce2d8546409bbf441e6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:17:56 -0400 Subject: [PATCH 7/9] docs(schema): document boolean schema serialization and deserialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive documentation to the OpenApiSchema class summary explaining: - How boolean schemas deserialize: true → empty schema, false → not with empty - How to produce boolean schema-like behavior during serialization - Applicability to OpenAPI 3.1+ with JSON Schema 2020-12 support This clarifies the API contract for consumers working with boolean schemas. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 11252d3c4..40f24bdd0 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -11,6 +11,12 @@ namespace Microsoft.OpenApi { /// /// The Schema Object allows the definition of input and output data types. + /// + /// For OpenAPI 3.1+ (JSON Schema 2020-12), this class supports boolean schemas: + /// - Deserialization: The boolean literal true deserializes to an empty schema (allows any value). + /// The boolean literal false deserializes to a schema with set to an empty schema (disallows any value). + /// - Serialization: To produce something functionally equivalent to boolean schemas, create an empty + /// for "true" behavior, or create a schema with only set to an empty schema for "false" behavior. /// public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer { From 7316e3f0e7b9acc11bc1e0255d7ca33837a1bbdc Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:24:37 -0400 Subject: [PATCH 8/9] perf(schema): optimize boolean schema deserialization Add generic GetValue() method to ValueNode to allow direct type access without string conversion. Update V31 and V32 schema deserializers to use GetValue() directly instead of converting to string and parsing with bool.TryParse(). This eliminates unnecessary string allocations and conversions for boolean schema values, improving performance in the deserialization hot path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Reader/ParseNodes/ValueNode.cs | 11 +++++++++++ .../Reader/V31/OpenApiSchemaDeserializer.cs | 18 +++++++----------- .../Reader/V32/OpenApiSchemaDeserializer.cs | 18 +++++++----------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/ValueNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/ValueNode.cs index a25a6275e..bf207d741 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/ValueNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/ValueNode.cs @@ -28,6 +28,17 @@ public override string GetScalarValue() ?? throw new OpenApiReaderException($"Expected a value at {Context.GetLocation()}."); } + /// + /// Attempts to get the underlying value directly as the specified type without string conversion. + /// + /// The type to retrieve the value as. + /// The retrieved value if successful. + /// True if the value was successfully converted to the specified type; otherwise, false. + public bool TryGetValue(out T? value) + { + return _node.TryGetValue(out value); + } + /// /// Create a /// diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index 65da0aa59..39c32c979 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -363,20 +363,16 @@ internal static partial class OpenApiV31Deserializer public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocument) { // Handle boolean schemas (true/false) for JSON Schema 2020-12 compatibility - if (node is ValueNode valueNode) + if (node is ValueNode valueNode && valueNode.TryGetValue(out var boolValue)) { - var value = valueNode.GetScalarValue(); - if (value != null && bool.TryParse(value, out var boolValue)) + var boolSchema = new OpenApiSchema(); + if (!boolValue) { - var boolSchema = new OpenApiSchema(); - if (!boolValue) - { - // false schema: represents "not valid" -> convert to "not: {}" - boolSchema.Not = new OpenApiSchema(); - } - // true schema: represents "always valid" -> return empty schema (default) - return boolSchema; + // false schema: represents "not valid" -> convert to "not: {}" + boolSchema.Not = new OpenApiSchema(); } + // true schema: represents "always valid" -> return empty schema (default) + return boolSchema; } var mapNode = node.CheckMapNode(OpenApiConstants.Schema); diff --git a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs index b880806e9..c18c48462 100644 --- a/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs @@ -363,20 +363,16 @@ internal static partial class OpenApiV32Deserializer public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocument) { // Handle boolean schemas (true/false) for JSON Schema 2020-12 compatibility - if (node is ValueNode valueNode) + if (node is ValueNode valueNode && valueNode.TryGetValue(out var boolValue)) { - var value = valueNode.GetScalarValue(); - if (value != null && bool.TryParse(value, out var boolValue)) + var boolSchema = new OpenApiSchema(); + if (!boolValue) { - var boolSchema = new OpenApiSchema(); - if (!boolValue) - { - // false schema: represents "not valid" -> convert to "not: {}" - boolSchema.Not = new OpenApiSchema(); - } - // true schema: represents "always valid" -> return empty schema (default) - return boolSchema; + // false schema: represents "not valid" -> convert to "not: {}" + boolSchema.Not = new OpenApiSchema(); } + // true schema: represents "always valid" -> return empty schema (default) + return boolSchema; } var mapNode = node.CheckMapNode(OpenApiConstants.Schema); From a54c0fd8fecb26931b5dd67cbd751085ccd4b431 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 27 Apr 2026 09:29:25 -0400 Subject: [PATCH 9/9] test(schema): rename deserialization test for clarity Rename DeserializeBasicSchemaWorks to DeserializeTrueSchemaParsesAsEmptySchema to better describe what the test validates - that boolean true values and empty schemas deserialize to the same result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../V31Tests/OpenApiSchemaTests.cs | 2 +- .../V32Tests/OpenApiSchemaTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index e743732f9..b65b2d8c3 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -848,7 +848,7 @@ public void ParseSchemaWithoutUnevaluatedPropertiesDefaultsToTrue() [Theory] [InlineData("{}")] [InlineData("true")] - public void DeserializeBasicSchemaWorks(string schemaSource) + public void DeserializeTrueSchemaParsesAsEmptySchema(string schemaSource) { // Arrange & Act var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _, OpenApiConstants.Json); diff --git a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs index d2a4d8569..7f91b0327 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V32Tests/OpenApiSchemaTests.cs @@ -699,7 +699,7 @@ public void ParseSchemaWithUnevaluatedPropertiesComplexSchema() [Theory] [InlineData("{}")] [InlineData("true")] - public void DeserializeBasicSchemaWorks(string schemaSource) + public void DeserializeTrueSchemaParsesAsEmptySchema(string schemaSource) { // Arrange & Act var schema = OpenApiModelFactory.Parse(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _, OpenApiConstants.Json);