diff --git a/global.json b/global.json index 459fe3e6e..d72c3f669 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.418" + "version": "8.0.419" } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 85f899bf5..fd802f13a 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -542,19 +542,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 @@ -594,19 +599,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)); @@ -827,19 +836,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 diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 1e924cad3..c6a198918 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -1308,6 +1308,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)]