Visitar URL original
fix: null reference exception for boolean component schemas by baywet · Pull Request #2839 · microsoft/OpenAPI.NET · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
{
/// <summary>
/// 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 <c>true</c> deserializes to an empty schema (allows any value).
/// The boolean literal <c>false</c> deserializes to a schema with <see cref="Not"/> set to an empty schema (disallows any value).
/// - Serialization: To produce something functionally equivalent to boolean schemas, create an empty <see cref="OpenApiSchema"/>
/// for "true" behavior, or create a schema with only <see cref="Not"/> set to an empty schema for "false" behavior.
/// </summary>
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
{
Expand Down Expand Up @@ -267,7 +273,7 @@
/// Initializes a copy of <see cref="IOpenApiSchema"/> object
/// </summary>
/// <param name="schema">The schema object to copy from.</param>
internal OpenApiSchema(IOpenApiSchema schema)

Check warning on line 276 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this constructor to reduce its Cognitive Complexity from 21 to the 15 allowed.
{
Utils.CheckArgumentNull(schema);
Title = schema.Title ?? Title;
Expand Down Expand Up @@ -348,11 +354,11 @@
SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer));
}

private static void SerializeBounds(IOpenApiWriter writer, OpenApiSpecVersion version, string propertyName, string exclusivePropertyName, string isExclusivePropertyName, string? value, string? exclusiveValue, bool? isExclusiveValue)

Check warning on line 357 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Method has 8 parameters, which is greater than the 7 authorized.

Check warning on line 357 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.
{
if (version >= OpenApiSpecVersion.OpenApi3_1)
{
if (!string.IsNullOrEmpty(exclusiveValue) && exclusiveValue is not null)

Check warning on line 361 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'.
{
// was explicitly set in the document or object model
writer.WritePropertyName(exclusivePropertyName);
Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.OpenApi/Reader/ParseNodes/ValueNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ public override string GetScalarValue()
?? throw new OpenApiReaderException($"Expected a value at {Context.GetLocation()}.");
}

/// <summary>
/// Attempts to get the underlying value directly as the specified type without string conversion.
/// </summary>
/// <typeparam name="T">The type to retrieve the value as.</typeparam>
/// <param name="value">The retrieved value if successful.</param>
/// <returns>True if the value was successfully converted to the specified type; otherwise, false.</returns>
public bool TryGetValue<T>(out T? value)
{
return _node.TryGetValue(out value);
}

/// <summary>
/// Create a <see cref="JsonNode"/>
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace Microsoft.OpenApi.Reader.V31;
internal static partial class OpenApiV31Deserializer
{
private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new()

Check warning on line 13 in src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this field to reduce its Cognitive Complexity from 57 to the 15 allowed.
{
{
"title",
Expand Down Expand Up @@ -360,8 +360,21 @@
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
};

public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocument)

Check failure on line 363 in src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AZ3PHVYXlh5VyPuocU2f&open=AZ3PHVYXlh5VyPuocU2f&pullRequest=2839
{
// Handle boolean schemas (true/false) for JSON Schema 2020-12 compatibility
if (node is ValueNode valueNode && valueNode.TryGetValue<bool>(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();
Expand Down
13 changes: 13 additions & 0 deletions src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
namespace Microsoft.OpenApi.Reader.V32;
internal static partial class OpenApiV32Deserializer
{
private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new()

Check warning on line 13 in src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this field to reduce its Cognitive Complexity from 57 to the 15 allowed.
{
{
"title",
Expand Down Expand Up @@ -360,8 +360,21 @@
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _) => o.AddExtension(p, LoadExtension(p,n))}
};

public static IOpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocument)

Check failure on line 363 in src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AZ3PHVUflh5VyPuocU2e&open=AZ3PHVUflh5VyPuocU2e&pullRequest=2839
{
// Handle boolean schemas (true/false) for JSON Schema 2020-12 compatibility
if (node is ValueNode valueNode && valueNode.TryGetValue<bool>(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();
Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
/// Registers a document's components into the workspace
/// </summary>
/// <param name="document"></param>
public void RegisterComponents(OpenApiDocument document)

Check warning on line 84 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 67 to the 15 allowed.
{
if (document?.Components == null) return;

Expand All @@ -91,111 +91,122 @@
// Register Schema
if (document.Components.Schemas != null)
{
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);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register Parameters
if (document.Components.Parameters != null)
{
foreach (var item in document.Components.Parameters)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.Parameter.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register Responses
if (document.Components.Responses != null)
{
foreach (var item in document.Components.Responses)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.Response.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register RequestBodies
if (document.Components.RequestBodies != null)
{
foreach (var item in document.Components.RequestBodies)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.RequestBody.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register Links
if (document.Components.Links != null)
{
foreach (var item in document.Components.Links)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.Link.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register Callbacks
if (document.Components.Callbacks != null)
{
foreach (var item in document.Components.Callbacks)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.Callback.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register PathItems
if (document.Components.PathItems != null)
{
foreach (var item in document.Components.PathItems)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.PathItem.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register Examples
if (document.Components.Examples != null)
{
foreach (var item in document.Components.Examples)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.Example.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register Headers
if (document.Components.Headers != null)
{
foreach (var item in document.Components.Headers)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.Header.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register SecuritySchemes
if (document.Components.SecuritySchemes != null)
{
foreach (var item in document.Components.SecuritySchemes)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.SecurityScheme.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}

// Register MediaTypes
if (document.Components.MediaTypes != null)
{
foreach (var item in document.Components.MediaTypes)
{
if (item.Value == null) continue;
location = baseUri + ReferenceType.MediaType.GetDisplayName() + ComponentSegmentSeparator + item.Key;
RegisterComponent(location, item.Value);
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.
}
}

Expand Down Expand Up @@ -279,7 +290,7 @@
/// <param name="value"></param>
public void AddDocumentId(string? key, Uri? value)
{
if (!string.IsNullOrEmpty(key) && key is not null && value is not null && !_documentsIdRegistry.ContainsKey(key))

Check warning on line 293 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'.
{
_documentsIdRegistry[key] = value;
}
Expand Down Expand Up @@ -382,7 +393,7 @@
return default;
}

internal static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments, Stack<IOpenApiSchema> visitedSchemas)

Check warning on line 396 in src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.
{
// Prevent infinite recursion in case of circular references
if (visitedSchemas.Contains(schema))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<NullReferenceException>(exception);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -844,5 +844,34 @@ public void ParseSchemaWithoutUnevaluatedPropertiesDefaultsToTrue()
Assert.Equivalent(expected, actual);
Assert.True(actual.UnevaluatedProperties); // Explicitly verify the default
}

[Theory]
[InlineData("{}")]
[InlineData("true")]
public void DeserializeTrueSchemaParsesAsEmptySchema(string schemaSource)
{
// Arrange & Act
var schema = OpenApiModelFactory.Parse<OpenApiSchema>(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _, OpenApiConstants.Json);

// Assert - schema should deserialize without error
Assert.NotNull(schema);
}

[Fact]
public void DeserializeFalseSchemaParsesAsNotEmptySchema()
{
// Arrange
var schemaSource = "false";

// Act
var schema = OpenApiModelFactory.Parse<OpenApiSchema>(schemaSource, OpenApiSpecVersion.OpenApi3_1, new(), out _, OpenApiConstants.Json);

// 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 ?? []);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NullReferenceException>(exception);
}
}
}
}

Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -695,6 +695,35 @@ public void ParseSchemaWithUnevaluatedPropertiesComplexSchema()
// Assert
Assert.Equivalent(expected, actual);
}

[Theory]
[InlineData("{}")]
[InlineData("true")]
public void DeserializeTrueSchemaParsesAsEmptySchema(string schemaSource)
{
// Arrange & Act
var schema = OpenApiModelFactory.Parse<OpenApiSchema>(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _, OpenApiConstants.Json);

// Assert - schema should deserialize without error
Assert.NotNull(schema);
}

[Fact]
public void DeserializeFalseSchemaParsesAsNotEmptySchema()
{
// Arrange
var schemaSource = "false";

// Act
var schema = OpenApiModelFactory.Parse<OpenApiSchema>(schemaSource, OpenApiSpecVersion.OpenApi3_2, new(), out _, OpenApiConstants.Json);

// 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 ?? []);
}
}
}

10 changes: 7 additions & 3 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading