Skip to content

Commit 5d58a8d

Browse files
authored
Merge pull request #2776 from microsoft/fix/unevaluated-object-to-v2
fix/unevaluated object to v2
2 parents 5fd43bf + 60c9adf commit 5d58a8d

File tree

3 files changed

+155
-36
lines changed

3 files changed

+155
-36
lines changed

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"sdk": {
3-
"version": "8.0.418"
3+
"version": "8.0.419"
44
}
55
}

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

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

560565
// Write patternProperties as an extension
@@ -594,19 +599,23 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
594599
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
595600
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
596601

597-
// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean
598-
if (UnevaluatedPropertiesSchema is not null)
599-
{
600-
writer.WriteOptionalObject(
601-
OpenApiConstants.UnevaluatedProperties,
602-
UnevaluatedPropertiesSchema,
603-
(w, s) => s.SerializeAsV31(w));
604-
}
605-
else if (!UnevaluatedProperties)
602+
// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean.
603+
// Only emit when the type could include objects.
604+
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
605+
if (!Type.HasValue || (Type.Value & JsonSchemaType.Object) != 0)
606606
{
607-
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties);
607+
if (UnevaluatedPropertiesSchema is not null)
608+
{
609+
writer.WriteOptionalObject(
610+
OpenApiConstants.UnevaluatedProperties,
611+
UnevaluatedPropertiesSchema,
612+
(w, s) => s.SerializeAsV31(w));
613+
}
614+
else if (!UnevaluatedProperties)
615+
{
616+
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties);
617+
}
608618
}
609-
// true is the default, no need to write it out
610619
writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s));
611620
writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w));
612621
writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s));
@@ -827,19 +836,24 @@ private void SerializeAsV2(
827836
// x-nullable extension
828837
SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0);
829838

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

845859
// 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
@@ -1308,6 +1308,111 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions(
13081308
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
13091309
}
13101310

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

0 commit comments

Comments
 (0)