Skip to content

Commit 852fb4c

Browse files
authored
Merge pull request #2774 from microsoft/copilot/fix-unevaluated-properties-serialization
fix(library): do not emit unevaluatedProperties for non-object schemas
2 parents 9e0e7a6 + b89f503 commit 852fb4c

File tree

2 files changed

+154
-35
lines changed

2 files changed

+154
-35
lines changed

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -547,19 +547,24 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
547547
// For versions < 3.1, write unevaluatedProperties as an extension
548548
if (version < OpenApiSpecVersion.OpenApi3_1)
549549
{
550-
// Write UnevaluatedPropertiesSchema as extension if present
551-
if (UnevaluatedPropertiesSchema is not null)
550+
// Only emit unevaluatedProperties when the type could include objects.
551+
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
552+
if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0)
552553
{
553-
writer.WriteOptionalObject(
554-
OpenApiConstants.UnevaluatedPropertiesExtension,
555-
UnevaluatedPropertiesSchema,
556-
callback);
557-
}
558-
// Write boolean false as extension if explicitly set to false
559-
else if (!UnevaluatedProperties)
560-
{
561-
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
562-
writer.WriteValue(false);
554+
// Write UnevaluatedPropertiesSchema as extension if present
555+
if (UnevaluatedPropertiesSchema is not null)
556+
{
557+
writer.WriteOptionalObject(
558+
OpenApiConstants.UnevaluatedPropertiesExtension,
559+
UnevaluatedPropertiesSchema,
560+
callback);
561+
}
562+
// Write boolean false as extension if explicitly set to false
563+
else if (!UnevaluatedProperties)
564+
{
565+
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
566+
writer.WriteValue(false);
567+
}
563568
}
564569

565570
// Write patternProperties as an extension
@@ -599,19 +604,23 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
599604
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
600605
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
601606

602-
// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean
603-
if (UnevaluatedPropertiesSchema is not null)
604-
{
605-
writer.WriteOptionalObject(
606-
OpenApiConstants.UnevaluatedProperties,
607-
UnevaluatedPropertiesSchema,
608-
(w, s) => s.SerializeAsV31(w));
609-
}
610-
else if (!UnevaluatedProperties)
607+
// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean.
608+
// Only emit when the type could include objects.
609+
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
610+
if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0)
611611
{
612-
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties);
612+
if (UnevaluatedPropertiesSchema is not null)
613+
{
614+
writer.WriteOptionalObject(
615+
OpenApiConstants.UnevaluatedProperties,
616+
UnevaluatedPropertiesSchema,
617+
(w, s) => s.SerializeAsV31(w));
618+
}
619+
else if (!UnevaluatedProperties)
620+
{
621+
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties);
622+
}
613623
}
614-
// true is the default, no need to write it out
615624
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
616625
writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w));
617626
writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s));
@@ -832,19 +841,24 @@ private void SerializeAsV2(
832841
// x-nullable extension
833842
SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0);
834843

835-
// Write UnevaluatedPropertiesSchema as extension if present
836-
if (UnevaluatedPropertiesSchema is not null)
844+
// Write UnevaluatedPropertiesSchema as extension if present.
845+
// Only emit when the type could include objects.
846+
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
847+
if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0)
837848
{
838-
writer.WriteOptionalObject(
839-
OpenApiConstants.UnevaluatedPropertiesExtension,
840-
UnevaluatedPropertiesSchema,
841-
(w, s) => s.SerializeAsV2(w));
842-
}
843-
// Write boolean false as extension if explicitly set to false
844-
else if (!UnevaluatedProperties)
845-
{
846-
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
847-
writer.WriteValue(false);
849+
if (UnevaluatedPropertiesSchema is not null)
850+
{
851+
writer.WriteOptionalObject(
852+
OpenApiConstants.UnevaluatedPropertiesExtension,
853+
UnevaluatedPropertiesSchema,
854+
(w, s) => s.SerializeAsV2(w));
855+
}
856+
// Write boolean false as extension if explicitly set to false
857+
else if (!UnevaluatedProperties)
858+
{
859+
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
860+
writer.WriteValue(false);
861+
}
848862
}
849863

850864
// Write patternProperties as an extension

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,111 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions(
13121312
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
13131313
}
13141314

1315+
[Theory]
1316+
[InlineData(JsonSchemaType.Array, "array")]
1317+
[InlineData(JsonSchemaType.String, "string")]
1318+
[InlineData(JsonSchemaType.Number, "number")]
1319+
[InlineData(JsonSchemaType.Integer, "integer")]
1320+
[InlineData(JsonSchemaType.Boolean, "boolean")]
1321+
[InlineData(JsonSchemaType.Null, "null")]
1322+
public async Task SerializeUnevaluatedPropertiesFalseNotEmittedForNonObjectType(JsonSchemaType nonObjectType, string typeName)
1323+
{
1324+
var expected = $@"{{ ""type"": ""{typeName}"" }}";
1325+
// Given - unevaluatedProperties should not be emitted when type is explicitly set to a non-object type
1326+
var schema = new OpenApiSchema
1327+
{
1328+
Type = nonObjectType,
1329+
UnevaluatedProperties = false
1330+
};
1331+
1332+
// When
1333+
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);
1334+
1335+
// Then
1336+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
1337+
}
1338+
1339+
[Theory]
1340+
[InlineData(JsonSchemaType.Array, "array")]
1341+
[InlineData(JsonSchemaType.String, "string")]
1342+
[InlineData(JsonSchemaType.Number, "number")]
1343+
[InlineData(JsonSchemaType.Integer, "integer")]
1344+
[InlineData(JsonSchemaType.Boolean, "boolean")]
1345+
[InlineData(JsonSchemaType.Null, "null")]
1346+
public async Task SerializeUnevaluatedPropertiesSchemaNotEmittedForNonObjectType(JsonSchemaType nonObjectType, string typeName)
1347+
{
1348+
var expected = $@"{{ ""type"": ""{typeName}"" }}";
1349+
// Given - unevaluatedProperties schema should not be emitted when type is explicitly set to a non-object type
1350+
var schema = new OpenApiSchema
1351+
{
1352+
Type = nonObjectType,
1353+
UnevaluatedPropertiesSchema = new OpenApiSchema { Type = JsonSchemaType.String }
1354+
};
1355+
1356+
// When
1357+
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);
1358+
1359+
// Then
1360+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
1361+
}
1362+
1363+
[Theory]
1364+
[InlineData(OpenApiSpecVersion.OpenApi2_0, JsonSchemaType.Array)]
1365+
[InlineData(OpenApiSpecVersion.OpenApi2_0, JsonSchemaType.String)]
1366+
[InlineData(OpenApiSpecVersion.OpenApi3_0, JsonSchemaType.Array)]
1367+
[InlineData(OpenApiSpecVersion.OpenApi3_0, JsonSchemaType.String)]
1368+
public async Task SerializeUnevaluatedPropertiesNotEmittedAsExtensionForNonObjectType(OpenApiSpecVersion version, JsonSchemaType nonObjectType)
1369+
{
1370+
// Given - unevaluatedProperties should not be emitted as extension when type is a non-object type
1371+
var schema = new OpenApiSchema
1372+
{
1373+
Type = nonObjectType,
1374+
UnevaluatedProperties = false
1375+
};
1376+
1377+
// When
1378+
var actual = await schema.SerializeAsJsonAsync(version);
1379+
1380+
// Then - should not contain unevaluatedProperties extension
1381+
var parsed = JsonNode.Parse(actual)!.AsObject();
1382+
Assert.False(parsed.ContainsKey(OpenApiConstants.UnevaluatedPropertiesExtension));
1383+
}
1384+
1385+
[Fact]
1386+
public async Task SerializeUnevaluatedPropertiesFalseStillEmittedForObjectType()
1387+
{
1388+
var expected = @"{ ""type"": ""object"", ""unevaluatedProperties"": false }";
1389+
// Given - unevaluatedProperties should still be emitted for object type
1390+
var schema = new OpenApiSchema
1391+
{
1392+
Type = JsonSchemaType.Object,
1393+
UnevaluatedProperties = false
1394+
};
1395+
1396+
// When
1397+
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);
1398+
1399+
// Then
1400+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
1401+
}
1402+
1403+
[Fact]
1404+
public async Task SerializeUnevaluatedPropertiesFalseStillEmittedWhenTypeNotSet()
1405+
{
1406+
var expected = @"{ ""unevaluatedProperties"": false }";
1407+
// Given - unevaluatedProperties should still be emitted when type is not explicitly set
1408+
var schema = new OpenApiSchema
1409+
{
1410+
UnevaluatedProperties = false
1411+
};
1412+
1413+
// When
1414+
var actual = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1);
1415+
1416+
// Then
1417+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
1418+
}
1419+
13151420
// PatternProperties tests
13161421
[Theory]
13171422
[InlineData(OpenApiSpecVersion.OpenApi3_1)]

0 commit comments

Comments
 (0)