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
84 changes: 49 additions & 35 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -547,19 +547,24 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
// 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)
// Only emit unevaluatedProperties when the type could include objects.
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0)
{
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);
// 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);
}
}

// Write patternProperties as an extension
Expand Down Expand Up @@ -599,19 +604,23 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);

// 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)
// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean.
// Only emit when the type could include objects.
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0)
{
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties);
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 @@ -832,19 +841,24 @@ private void SerializeAsV2(
// x-nullable extension
SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0);

// Write UnevaluatedPropertiesSchema as extension if present
if (UnevaluatedPropertiesSchema is not null)
// Write UnevaluatedPropertiesSchema as extension if present.
// Only emit when the type could include objects.
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0)
{
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);
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);
}
}

// Write patternProperties as an extension
Expand Down
105 changes: 105 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,111 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions(
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Theory]
[InlineData(JsonSchemaType.Array, "array")]
[InlineData(JsonSchemaType.String, "string")]
[InlineData(JsonSchemaType.Number, "number")]
[InlineData(JsonSchemaType.Integer, "integer")]
[InlineData(JsonSchemaType.Boolean, "boolean")]
[InlineData(JsonSchemaType.Null, "null")]
public async Task SerializeUnevaluatedPropertiesFalseNotEmittedForNonObjectType(JsonSchemaType nonObjectType, string typeName)
{
var expected = $@"{{ ""type"": ""{typeName}"" }}";
// Given - unevaluatedProperties should not be emitted when type is explicitly set to a non-object type
var schema = new OpenApiSchema
{
Type = nonObjectType,
UnevaluatedProperties = false
};

// When
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);

// Then
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Theory]
[InlineData(JsonSchemaType.Array, "array")]
[InlineData(JsonSchemaType.String, "string")]
[InlineData(JsonSchemaType.Number, "number")]
[InlineData(JsonSchemaType.Integer, "integer")]
[InlineData(JsonSchemaType.Boolean, "boolean")]
[InlineData(JsonSchemaType.Null, "null")]
public async Task SerializeUnevaluatedPropertiesSchemaNotEmittedForNonObjectType(JsonSchemaType nonObjectType, string typeName)
{
var expected = $@"{{ ""type"": ""{typeName}"" }}";
// Given - unevaluatedProperties schema should not be emitted when type is explicitly set to a non-object type
var schema = new OpenApiSchema
{
Type = nonObjectType,
UnevaluatedPropertiesSchema = new OpenApiSchema { Type = JsonSchemaType.String }
};

// When
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);

// Then
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Theory]
[InlineData(OpenApiSpecVersion.OpenApi2_0, JsonSchemaType.Array)]
[InlineData(OpenApiSpecVersion.OpenApi2_0, JsonSchemaType.String)]
[InlineData(OpenApiSpecVersion.OpenApi3_0, JsonSchemaType.Array)]
[InlineData(OpenApiSpecVersion.OpenApi3_0, JsonSchemaType.String)]
public async Task SerializeUnevaluatedPropertiesNotEmittedAsExtensionForNonObjectType(OpenApiSpecVersion version, JsonSchemaType nonObjectType)
{
// Given - unevaluatedProperties should not be emitted as extension when type is a non-object type
var schema = new OpenApiSchema
{
Type = nonObjectType,
UnevaluatedProperties = false
};

// When
var actual = await schema.SerializeAsJsonAsync(version);

// Then - should not contain unevaluatedProperties extension
var parsed = JsonNode.Parse(actual)!.AsObject();
Assert.False(parsed.ContainsKey(OpenApiConstants.UnevaluatedPropertiesExtension));
}

[Fact]
public async Task SerializeUnevaluatedPropertiesFalseStillEmittedForObjectType()
{
var expected = @"{ ""type"": ""object"", ""unevaluatedProperties"": false }";
// Given - unevaluatedProperties should still be emitted for object type
var schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
UnevaluatedProperties = false
};

// When
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);

// Then
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Fact]
public async Task SerializeUnevaluatedPropertiesFalseStillEmittedWhenTypeNotSet()
{
var expected = @"{ ""unevaluatedProperties"": false }";
// Given - unevaluatedProperties should still be emitted when type is not explicitly set
var schema = new OpenApiSchema
{
UnevaluatedProperties = false
};

// When
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);

// Then
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

// PatternProperties tests
[Theory]
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
Expand Down
Loading