Skip to content
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,7 +17,8 @@ namespace Microsoft.OpenApi
public static class OpenApiSchemaRules
{
/// <summary>
/// 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.
/// </summary>
public static ValidationRule<IOpenApiSchema> ValidateSchemaPropertyHasValue =>
new(nameof(ValidateSchemaPropertyHasValue),
Expand All @@ -28,13 +29,15 @@ 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,
schema is OpenApiSchemaReference { Reference: not null } schemaReference
? schemaReference.Reference.Id
: string.Empty, property.Key));
context.Exit();
context.Exit();
}
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -209,7 +210,7 @@ public void ValidateNullProperty()
Assert.NotEmpty(validator.Errors);
Assert.Equivalent(new List<OpenApiValidatorError>
{
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);
Expand Down Expand Up @@ -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<string, IOpenApiSchema>
{
["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<string, IOpenApiSchema>
{
["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<string, IOpenApiSchema>
{
["User"] = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema>
{
["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, OpenApiOperation>
{
[HttpMethod.Get] = new OpenApiOperation
{
Responses = new OpenApiResponses
{
["200"] = new OpenApiResponse
{
Description = "OK",
Content = new Dictionary<string, IOpenApiMediaType>
{
["application/json"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Properties = new Dictionary<string, IOpenApiSchema>
{
["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();
Expand Down