diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs index eff687253..b062741b2 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -17,7 +17,8 @@ namespace Microsoft.OpenApi public static class OpenApiSchemaRules { /// - /// Validates Schema Property has value + /// Validates that each property defined in a schema's properties dictionary has a non-null schema value. + /// When a property entry value is null, reports a validation error with a JSON pointer anchored under the properties segment. /// public static ValidationRule ValidateSchemaPropertyHasValue => new(nameof(ValidateSchemaPropertyHasValue), @@ -28,6 +29,7 @@ public static class OpenApiSchemaRules foreach (var property in schema.Properties .Where(entry => entry.Value is null)) { + context.Enter(OpenApiConstants.Properties); context.Enter(property.Key); context.CreateError(nameof(ValidateSchemaPropertyHasValue), string.Format(SRResource.Validation_SchemaPropertyObjectRequired, @@ -35,6 +37,7 @@ public static class OpenApiSchemaRules ? schemaReference.Reference.Id : string.Empty, property.Key)); context.Exit(); + context.Exit(); } } }); diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs index 7f5cd47e2..c0ecc7888 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text.Json.Nodes; using Xunit; @@ -209,7 +210,7 @@ public void ValidateNullProperty() Assert.NotEmpty(validator.Errors); Assert.Equivalent(new List { - new OpenApiValidatorError(nameof(OpenApiSchemaRules.ValidateSchemaPropertyHasValue),"#/schemas/schema1/property1", + new OpenApiValidatorError(nameof(OpenApiSchemaRules.ValidateSchemaPropertyHasValue),"#/schemas/schema1/properties/property1", string.Format(SRResource.Validation_SchemaPropertyObjectRequired, string.Empty, "property1")) }, validator.Errors); @@ -557,6 +558,146 @@ public void ValidateSchemaDiscriminatorChecksOneOfAnyOfAndAllOf() Assert.Empty(validator.Errors); } + [Fact] + public void ValidateSchemaPropertyHasValueShouldReportErrorWithPropertiesPath() + { + // Arrange + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["id"] = new OpenApiSchema { Type = JsonSchemaType.Integer }, + ["name"] = null! + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk((IOpenApiSchema)schema); + + // Assert + var error = Assert.Single(validator.Errors); + Assert.Equal(nameof(OpenApiSchemaRules.ValidateSchemaPropertyHasValue), error.RuleName); + Assert.Equal("#/properties/name", error.Pointer); + } + + [Fact] + public void ValidateSchemaPropertyHasValueWithMultipleNullPropertiesShouldIncludePropertiesSegmentForEach() + { + // Arrange + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["first"] = null!, + ["valid"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["second"] = null! + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk((IOpenApiSchema)schema); + + // Assert + var errors = validator.Errors.ToList(); + Assert.Equal(2, errors.Count); + Assert.All(errors, e => Assert.Equal(nameof(OpenApiSchemaRules.ValidateSchemaPropertyHasValue), e.RuleName)); + Assert.Contains(errors, e => e.Pointer == "#/properties/first"); + Assert.Contains(errors, e => e.Pointer == "#/properties/second"); + } + + [Fact] + public void ValidateSchemaPropertyHasValueInDocumentComponentsShouldReportFullPointer() + { + // Arrange + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["User"] = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["nullProp"] = null! + } + } + } + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(document); + + // Assert + var error = Assert.Single(validator.Errors); + Assert.Equal(nameof(OpenApiSchemaRules.ValidateSchemaPropertyHasValue), error.RuleName); + Assert.Equal("#/components/schemas/User/properties/nullProp", error.Pointer); + } + + [Fact] + public void ValidateSchemaPropertyHasValueInOperationResponseShouldReportFullPointer() + { + // Arrange + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" }, + Paths = new OpenApiPaths + { + ["/items"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [HttpMethod.Get] = new OpenApiOperation + { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["missingSchema"] = null! + } + } + } + } + } + } + } + } + } + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(document); + + // Assert + var error = Assert.Single(validator.Errors); + Assert.Equal(nameof(OpenApiSchemaRules.ValidateSchemaPropertyHasValue), error.RuleName); + Assert.Equal("#/paths/~1items/get/responses/200/content/application~1json/schema/properties/missingSchema", error.Pointer); + } + private static OpenApiSchema CreateSchemaChain(int depth, string discriminatorName) { var root = new OpenApiSchema();