Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
11 changes: 9 additions & 2 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,15 @@
public IList<JsonNode>? Enum { get; }

/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-validation
/// </summary>
/// Indicates whether unevaluated properties are allowed. When false, no unevaluated properties are permitted.
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-unevaluatedproperties
/// Only serialized when false and UnevaluatedPropertiesSchema (from IOpenApiSchemaWithUnevaluatedProperties) is null.
/// </summary>
/// <remarks>
/// NOTE: This property differs from the naming pattern of AdditionalPropertiesAllowed for binary compatibility reasons.
/// In the next major version, this will be renamed to UnevaluatedPropertiesAllowed.
/// TODO: Rename to UnevaluatedPropertiesAllowed in the next major version.

Check warning on line 268 in src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
/// </remarks>
public bool UnevaluatedProperties { get; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Microsoft.OpenApi;

/// <summary>
/// Compatibility interface for UnevaluatedProperties schema support.
/// This interface provides access to the UnevaluatedPropertiesSchema property, which represents
/// the schema for unevaluated properties as defined in JSON Schema draft 2020-12.
///
/// NOTE: This is a temporary compatibility solution. In the next major version:
/// - This interface will be merged into IOpenApiSchema
/// - The UnevaluatedPropertiesSchema property will be renamed to UnevaluatedProperties
/// - The current UnevaluatedProperties boolean property will be renamed to UnevaluatedPropertiesAllowed
/// </summary>
/// <remarks>
/// TODO: Remove this interface in the next major version and merge its content into IOpenApiSchema.

Check warning on line 14 in src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithUnevaluatedProperties.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
/// </remarks>
public interface IOpenApiSchemaWithUnevaluatedProperties
{
/// <summary>
/// Follow JSON Schema definition: https://json-schema.org/draft/2020-12/json-schema-core#name-unevaluatedproperties
/// This is a schema that unevaluated properties must validate against.
/// When serialized, this takes precedence over the UnevaluatedProperties boolean property.
/// </summary>
/// <remarks>
/// NOTE: This property differs from the naming pattern of AdditionalProperties/AdditionalPropertiesAllowed
/// for binary compatibility reasons. In the next major version:
/// - This property will be renamed to UnevaluatedProperties
/// - The current boolean UnevaluatedProperties property will be renamed to UnevaluatedPropertiesAllowed
///
/// TODO: Rename this property to UnevaluatedProperties in the next major version.

Check warning on line 29 in src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchemaWithUnevaluatedProperties.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
/// </remarks>
IOpenApiSchema? UnevaluatedPropertiesSchema { get; }
}
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
/// </summary>
public const string UnevaluatedProperties = "unevaluatedProperties";

/// <summary>
/// Extension: x-jsonschema-unevaluatedProperties
/// </summary>
public const string UnevaluatedPropertiesExtension = "x-jsonschema-unevaluatedProperties";

/// <summary>
/// Field: Version
/// </summary>
Expand Down Expand Up @@ -743,17 +748,17 @@
/// <summary>
/// Field: V3 JsonSchema Reference Uri
/// </summary>
public const string V3ReferenceUri = "https://registry/components/schemas/";

Check warning on line 751 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

/// <summary>
/// Field: V2 JsonSchema Reference Uri
/// </summary>
public const string V2ReferenceUri = "https://registry/definitions/";

Check warning on line 756 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

/// <summary>
/// The default registry uri for OpenApi documents and workspaces
/// </summary>
public const string BaseRegistryUri = "https://openapi.net/";

Check warning on line 761 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

/// <summary>
/// The components path segment in a $ref value.
Expand Down
61 changes: 58 additions & 3 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/// <summary>
/// The Schema Object allows the definition of input and output data types.
/// </summary>
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IMetadataContainer
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
{
/// <inheritdoc />
public string? Title { get; set; }
Expand Down Expand Up @@ -232,7 +232,10 @@
public IList<JsonNode>? Enum { get; set; }

/// <inheritdoc />
public bool UnevaluatedProperties { get; set; }
public bool UnevaluatedProperties { get; set; } = true;

/// <inheritdoc />
public IOpenApiSchema? UnevaluatedPropertiesSchema { get; set; }

/// <inheritdoc />
public OpenApiExternalDocs? ExternalDocs { get; set; }
Expand Down Expand Up @@ -264,7 +267,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 270 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. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
Utils.CheckArgumentNull(schema);
Title = schema.Title ?? Title;
Expand All @@ -277,6 +280,10 @@
DynamicRef = schema.DynamicRef ?? DynamicRef;
Definitions = schema.Definitions != null ? new Dictionary<string, IOpenApiSchema>(schema.Definitions) : null;
UnevaluatedProperties = schema.UnevaluatedProperties;
if (schema is IOpenApiSchemaWithUnevaluatedProperties { UnevaluatedPropertiesSchema: { } unevaluatedSchema })
{
UnevaluatedPropertiesSchema = unevaluatedSchema.CreateShallowCopy();
}
ExclusiveMaximum = schema.ExclusiveMaximum ?? ExclusiveMaximum;
ExclusiveMinimum = schema.ExclusiveMinimum ?? ExclusiveMinimum;
if (schema is OpenApiSchema eMSchema)
Expand Down Expand Up @@ -537,9 +544,29 @@
// deprecated
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);

// For versions < 3.1, write unevaluatedProperties as an extension
if (version < OpenApiSpecVersion.OpenApi3_1)
{
// Write UnevaluatedPropertiesSchema as extension if present
if (UnevaluatedPropertiesSchema is not null)
{
writer.WriteOptionalObject(
OpenApiConstants.UnevaluatedPropertiesExtension,
UnevaluatedPropertiesSchema,
callback);
}
// Write boolean false as extension if explicitly set to false
else if (!UnevaluatedProperties)
{
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
writer.WriteValue(false);
}
}

// extensions
writer.WriteExtensions(Extensions, version);


// Unrecognized keywords
if (UnrecognizedKeywords is not null && UnrecognizedKeywords.Any())
{
Expand All @@ -565,7 +592,20 @@
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w));
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false);

// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean
if (UnevaluatedPropertiesSchema is not null)
{
writer.WriteOptionalObject(
OpenApiConstants.UnevaluatedProperties,
UnevaluatedPropertiesSchema,
(w, s) => s.SerializeAsV31(w));
}
else if (!UnevaluatedProperties)
{
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties);
}
// true is the default, no need to write it out
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w));
writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s));
Expand Down Expand Up @@ -780,6 +820,21 @@
// x-nullable extension
SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0);

// Write UnevaluatedPropertiesSchema as extension if present
if (UnevaluatedPropertiesSchema is not null)
{
writer.WriteOptionalObject(
OpenApiConstants.UnevaluatedPropertiesExtension,
UnevaluatedPropertiesSchema,
(w, s) => s.SerializeAsV2(w));
}
// Write boolean false as extension if explicitly set to false
else if (!UnevaluatedProperties)
{
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
writer.WriteValue(false);
}

// extensions
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.OpenApi
/// <summary>
/// Schema reference object
/// </summary>
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema
public class OpenApiSchemaReference : BaseOpenApiReferenceHolder<OpenApiSchema, IOpenApiSchema, JsonSchemaReference>, IOpenApiSchema, IOpenApiSchemaWithUnevaluatedProperties
{

/// <summary>
Expand Down Expand Up @@ -144,7 +144,9 @@ public IList<JsonNode>? Examples
/// <inheritdoc/>
public IList<JsonNode>? Enum { get => Target?.Enum; }
/// <inheritdoc/>
public bool UnevaluatedProperties { get => Target?.UnevaluatedProperties ?? false; }
public bool UnevaluatedProperties { get => Target?.UnevaluatedProperties ?? true; }
/// <inheritdoc/>
public IOpenApiSchema? UnevaluatedPropertiesSchema { get => (Target as IOpenApiSchemaWithUnevaluatedProperties)?.UnevaluatedPropertiesSchema; }
/// <inheritdoc/>
public OpenApiExternalDocs? ExternalDocs { get => Target?.ExternalDocs; }
/// <inheritdoc/>
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#nullable enable
const Microsoft.OpenApi.OpenApiConstants.UnevaluatedPropertiesExtension = "x-jsonschema-unevaluatedProperties" -> string!
Microsoft.OpenApi.IOpenApiSchemaWithUnevaluatedProperties
Microsoft.OpenApi.IOpenApiSchemaWithUnevaluatedProperties.UnevaluatedPropertiesSchema.get -> Microsoft.OpenApi.IOpenApiSchema?
Microsoft.OpenApi.OpenApiSchema.UnevaluatedPropertiesSchema.get -> Microsoft.OpenApi.IOpenApiSchema?
Microsoft.OpenApi.OpenApiSchema.UnevaluatedPropertiesSchema.set -> void
Microsoft.OpenApi.OpenApiSchemaReference.UnevaluatedPropertiesSchema.get -> Microsoft.OpenApi.IOpenApiSchema?
17 changes: 13 additions & 4 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. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
{
"title",
Expand Down Expand Up @@ -146,12 +146,21 @@
},
{
"unevaluatedProperties",
(o, n, _) =>
(o, n, t) =>
Comment thread
baywet marked this conversation as resolved.
{
var unevaluatedProps = n.GetScalarValue();
if (unevaluatedProps != null)
// Handle both boolean (false/true) and schema object cases
if (n is ValueNode)
{
var value = n.GetScalarValue();
if (value is not null)
{
o.UnevaluatedProperties = bool.Parse(value);
}
}
else
{
o.UnevaluatedProperties = bool.Parse(unevaluatedProps);
// Schema object case: deserialize as schema
o.UnevaluatedPropertiesSchema = LoadSchema(n, t);
}
}
},
Expand Down
17 changes: 13 additions & 4 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. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
{
"title",
Expand Down Expand Up @@ -146,12 +146,21 @@
},
{
"unevaluatedProperties",
(o, n, _) =>
(o, n, t) =>
{
var unevaluatedProps = n.GetScalarValue();
if (unevaluatedProps != null)
// Handle both boolean (false/true) and schema object cases
if (n is ValueNode)
{
var value = n.GetScalarValue();
if (value is not null)
{
o.UnevaluatedProperties = bool.Parse(value);
}
}
else
{
o.UnevaluatedProperties = bool.Parse(unevaluatedProps);
// Schema object case: deserialize as schema
o.UnevaluatedPropertiesSchema = LoadSchema(n, t);
}
}
},
Expand Down
137 changes: 137 additions & 0 deletions test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -707,5 +707,142 @@ public void ReturnSingleIdentifierWorks()
Assert.Equal("integer", type.ToSingleIdentifier());
Assert.Throws<InvalidOperationException>(() => types.ToSingleIdentifier());
}

// UnevaluatedProperties deserialization tests
[Fact]
public void ParseSchemaWithUnevaluatedPropertiesBooleanFalse()
{
// Arrange
var schema = @"{
""type"": ""object"",
""unevaluatedProperties"": false
}";

var expected = new OpenApiSchema()
{
Type = JsonSchemaType.Object,
UnevaluatedProperties = false
};

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

// Assert
Assert.Equivalent(expected, actual);
}

[Fact]
public void ParseSchemaWithUnevaluatedPropertiesBooleanTrue()
{
// Arrange - true should be parsed but is the default, effectively a no-op
var schema = @"{
""type"": ""object"",
Comment thread
baywet marked this conversation as resolved.
""unevaluatedProperties"": true
}";

var expected = new OpenApiSchema()
{
Type = JsonSchemaType.Object,
UnevaluatedProperties = true
};

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

// Assert
Assert.Equivalent(expected, actual);
}

[Fact]
public void ParseSchemaWithUnevaluatedPropertiesSchema()
{
// Arrange
var schema = @"{
""type"": ""object"",
""unevaluatedProperties"": {
""type"": ""string""
}
}";

var expected = new OpenApiSchema()
{
Type = JsonSchemaType.Object,
UnevaluatedPropertiesSchema = new OpenApiSchema
{
Type = JsonSchemaType.String
}
};

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

// Assert
Assert.Equivalent(expected, actual);
}

[Fact]
public void ParseSchemaWithUnevaluatedPropertiesComplexSchema()
{
// Arrange
var schema = @"{
""type"": ""object"",
""properties"": {
""name"": { ""type"": ""string"" }
},
""unevaluatedProperties"": {
""type"": ""number"",
""minimum"": ""0""
}
}";

var expected = new OpenApiSchema()
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema>
{
["name"] = new OpenApiSchema { Type = JsonSchemaType.String }
},
UnevaluatedPropertiesSchema = new OpenApiSchema
{
Type = JsonSchemaType.Number,
Minimum = "0"
}
};

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

// Assert
Assert.Equivalent(expected, actual);
}

[Fact]
public void ParseSchemaWithoutUnevaluatedPropertiesDefaultsToTrue()
{
// Arrange - no unevaluatedProperties property should default to true (allow all)
var schema = @"{
""type"": ""object"",
""properties"": {
""name"": { ""type"": ""string"" }
}
}";

var expected = new OpenApiSchema()
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema>
{
["name"] = new OpenApiSchema { Type = JsonSchemaType.String }
},
UnevaluatedProperties = true // Default value
};

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

// Assert
Assert.Equivalent(expected, actual);
Assert.True(actual.UnevaluatedProperties); // Explicitly verify the default
}
}
}
Loading
Loading