Skip to content
Merged
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