Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.418"
"version": "8.0.417"
Comment thread
baywet marked this conversation as resolved.
Outdated
}
}
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 interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte
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.
/// </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.
/// </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.
/// </remarks>
IOpenApiSchema? UnevaluatedPropertiesSchema { get; }
}
26 changes: 23 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 @@ namespace Microsoft.OpenApi
/// <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 string? Minimum
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 @@ -277,6 +280,10 @@ internal OpenApiSchema(IOpenApiSchema schema)
DynamicRef = schema.DynamicRef ?? DynamicRef;
Definitions = schema.Definitions != null ? new Dictionary<string, IOpenApiSchema>(schema.Definitions) : null;
UnevaluatedProperties = schema.UnevaluatedProperties;
if (schema is IOpenApiSchemaWithUnevaluatedProperties schemaWithUnevaluated)
{
UnevaluatedPropertiesSchema = schemaWithUnevaluated.UnevaluatedPropertiesSchema?.CreateShallowCopy();
}
Comment thread
baywet marked this conversation as resolved.
Outdated
ExclusiveMaximum = schema.ExclusiveMaximum ?? ExclusiveMaximum;
ExclusiveMinimum = schema.ExclusiveMinimum ?? ExclusiveMinimum;
if (schema is OpenApiSchema eMSchema)
Expand Down Expand Up @@ -565,7 +572,20 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
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
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
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
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 @@ -146,12 +146,21 @@ internal static partial class OpenApiV31Deserializer
},
{
"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
108 changes: 108 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,113 @@ 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);
}
}
}
Loading