diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs
index 87fb96bc32..e30a0f524b 100644
--- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs
+++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs
@@ -135,7 +135,7 @@ public void CreateDocument(bool doOverrideExistingDocument = false)
string url = string.IsNullOrEmpty(runtimeBaseRoute) ? restEndpointPath : runtimeBaseRoute + "/" + restEndpointPath;
OpenApiComponents components = new()
{
- Schemas = CreateComponentSchemas(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName)
+ Schemas = CreateComponentSchemas(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName, runtimeConfig.IsRequestBodyStrict)
};
// Collect all entity tags and their descriptions for the top-level tags array
@@ -976,9 +976,14 @@ private static OpenApiMediaType CreateResponseContainer(string responseObjectSch
/// 2) {EntityName}_NoAutoPK -> No auto-generated primary keys present in schema, used for POST requests where PK is not autogenerated and GET (all).
/// 3) {EntityName}_NoPK -> No primary keys present in schema, used for POST requests where PK is autogenerated and GET (all).
/// Schema objects can be referenced elsewhere in the OpenAPI document with the intent to reduce document verbosity.
+ /// When isRequestBodyStrict is false, request body schemas will have additionalProperties set to true
+ /// to indicate that extra fields are allowed and will be ignored.
///
+ /// Runtime entities from configuration.
+ /// Default data source name.
+ /// Whether extra fields are rejected in request bodies.
/// Collection of schemas for entities defined in the runtime configuration.
- private Dictionary CreateComponentSchemas(RuntimeEntities entities, string defaultDataSourceName)
+ private Dictionary CreateComponentSchemas(RuntimeEntities entities, string defaultDataSourceName, bool isRequestBodyStrict)
{
Dictionary schemas = new();
// for rest scenario we need the default datasource name.
@@ -1006,18 +1011,20 @@ private Dictionary CreateComponentSchemas(RuntimeEntities
if (dbObject.SourceType is EntitySourceType.StoredProcedure)
{
// Request body schema whose properties map to stored procedure parameters
+ // When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
DatabaseStoredProcedure spObject = (DatabaseStoredProcedure)dbObject;
- schemas.Add(entityName + SP_REQUEST_SUFFIX, CreateSpRequestComponentSchema(fields: spObject.StoredProcedureDefinition.Parameters));
+ schemas.Add(entityName + SP_REQUEST_SUFFIX, CreateSpRequestComponentSchema(fields: spObject.StoredProcedureDefinition.Parameters, allowAdditionalProperties: !isRequestBodyStrict));
// Response body schema whose properties map to the stored procedure's first result set columns
// as described by sys.dm_exec_describe_first_result_set.
- schemas.Add(entityName + SP_RESPONSE_SUFFIX, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
+ schemas.Add(entityName + SP_RESPONSE_SUFFIX, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: false));
}
else
{
// Create component schema for FULL entity with all primary key columns (included auto-generated)
// which will typically represent the response body of a request or a stored procedure's request body.
- schemas.Add(entityName, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
+ // When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
+ schemas.Add(entityName, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: !isRequestBodyStrict));
// Create an entity's request body component schema excluding autogenerated primary keys.
// A POST request requires any non-autogenerated primary key references to be in the request body.
@@ -1037,7 +1044,8 @@ private Dictionary CreateComponentSchemas(RuntimeEntities
}
}
- schemas.Add($"{entityName}_NoAutoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
+ // When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
+ schemas.Add($"{entityName}_NoAutoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: !isRequestBodyStrict));
// Create an entity's request body component schema excluding all primary keys
// by removing the tracked non-autogenerated primary key column names and removing them from
@@ -1053,7 +1061,8 @@ private Dictionary CreateComponentSchemas(RuntimeEntities
}
}
- schemas.Add($"{entityName}_NoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
+ // When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
+ schemas.Add($"{entityName}_NoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: !isRequestBodyStrict));
}
}
@@ -1066,10 +1075,10 @@ private Dictionary CreateComponentSchemas(RuntimeEntities
/// Additionally, the property typeMetadata is sourced by converting the stored procedure
/// parameter's SystemType to JsonDataType.
///
- ///
/// Collection of stored procedure parameter metadata.
+ /// When true, sets additionalProperties to true indicating extra fields are allowed.
/// OpenApiSchema object representing a stored procedure's request body.
- private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary fields)
+ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary fields, bool allowAdditionalProperties)
{
Dictionary properties = new();
HashSet required = new();
@@ -1097,7 +1106,8 @@ private static OpenApiSchema CreateSpRequestComponentSchema(DictionaryList of mapped (alias) field names.
/// Metadata provider for database objects.
/// Runtime entities from configuration.
+ /// When true, sets additionalProperties to true indicating extra fields are allowed.
/// Raised when an entity's database metadata can't be found,
/// indicating a failure due to the provided entityName.
/// Entity's OpenApiSchema representation.
- private static OpenApiSchema CreateComponentSchema(string entityName, HashSet fields, ISqlMetadataProvider metadataProvider, RuntimeEntities entities)
+ private static OpenApiSchema CreateComponentSchema(string entityName, HashSet fields, ISqlMetadataProvider metadataProvider, RuntimeEntities entities, bool allowAdditionalProperties)
{
if (!metadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null)
{
@@ -1166,7 +1177,8 @@ private static OpenApiSchema CreateComponentSchema(string entityName, HashSet GenerateOpenApiDocumentAsync(
RuntimeEntities runtimeEntities,
string configFileName,
string databaseEnvironment)
+ {
+ return await GenerateOpenApiDocumentAsync(
+ runtimeEntities: runtimeEntities,
+ configFileName: configFileName,
+ databaseEnvironment: databaseEnvironment,
+ restOptions: null);
+ }
+
+ ///
+ /// Bootstraps a test server instance using a runtime config file generated
+ /// from the provided entity collection and REST options. The test server is only used to generate
+ /// and return the OpenApiDocument for use this method's callers.
+ ///
+ ///
+ ///
+ ///
+ /// Optional REST runtime options to customize request-body-strict setting.
+ /// Generated OpenApiDocument
+ internal static async Task GenerateOpenApiDocumentAsync(
+ RuntimeEntities runtimeEntities,
+ string configFileName,
+ string databaseEnvironment,
+ RestRuntimeOptions restOptions)
{
TestHelper.SetupDatabaseEnvironment(databaseEnvironment);
FileSystem fileSystem = new();
FileSystemRuntimeConfigLoader loader = new(fileSystem);
loader.TryLoadKnownConfig(out RuntimeConfig config);
+ // Create custom REST options if provided, otherwise use existing config
+ RuntimeOptions runtimeWithRestOptions = restOptions is not null
+ ? config.Runtime with { Rest = restOptions, Host = config.Runtime?.Host with { Mode = HostMode.Production } }
+ : config.Runtime with { Host = config.Runtime?.Host with { Mode = HostMode.Production } };
+
RuntimeConfig configWithCustomHostMode = config with
{
- Runtime = config.Runtime with
- {
- Host = config.Runtime?.Host with { Mode = HostMode.Production }
- },
+ Runtime = runtimeWithRestOptions,
Entities = runtimeEntities
};
diff --git a/src/Service.Tests/OpenApiDocumentor/RequestBodyStrictTests.cs b/src/Service.Tests/OpenApiDocumentor/RequestBodyStrictTests.cs
new file mode 100644
index 0000000000..7a1622e353
--- /dev/null
+++ b/src/Service.Tests/OpenApiDocumentor/RequestBodyStrictTests.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Azure.DataApiBuilder.Config.ObjectModel;
+using Microsoft.OpenApi.Models;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration
+{
+ ///
+ /// Integration tests validating that OpenAPI document schemas respect the request-body-strict configuration.
+ /// When request-body-strict is false, schemas should have additionalProperties set to true to indicate
+ /// that extra fields in request bodies are allowed and will be ignored.
+ ///
+ [TestCategory(TestCategory.MSSQL)]
+ [TestClass]
+ public class RequestBodyStrictTests
+ {
+ private const string CUSTOM_CONFIG = "request-body-strict.MsSql.json";
+ private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL;
+
+ ///
+ /// Validates that when request-body-strict is true (default), the OpenAPI schema has
+ /// additionalProperties set to false, indicating that extra fields are not allowed.
+ ///
+ [TestMethod]
+ public async Task OpenApiSchema_WhenRequestBodyStrictTrue_AdditionalPropertiesIsFalse()
+ {
+ // Arrange
+ Entity entity = new(
+ Source: new(Object: "books", EntitySourceType.Table, null, null),
+ Fields: null,
+ GraphQL: new(Singular: null, Plural: null, Enabled: false),
+ Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
+ Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
+ Mappings: null,
+ Relationships: null);
+
+ Dictionary entities = new()
+ {
+ { "Book", entity }
+ };
+
+ RuntimeEntities runtimeEntities = new(entities);
+
+ // request-body-strict: true (default)
+ RestRuntimeOptions restOptions = new(Enabled: true, Path: "/api", RequestBodyStrict: true);
+
+ // Act - Create OpenApi document
+ OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync(
+ runtimeEntities: runtimeEntities,
+ configFileName: CUSTOM_CONFIG,
+ databaseEnvironment: MSSQL_ENVIRONMENT,
+ restOptions: restOptions);
+
+ // Assert - Validate that Book schema has additionalProperties set to false
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas.ContainsKey("Book"),
+ "Schema 'Book' should exist in OpenAPI document.");
+
+ OpenApiSchema bookSchema = openApiDocument.Components.Schemas["Book"];
+ Assert.IsFalse(
+ bookSchema.AdditionalPropertiesAllowed,
+ "When request-body-strict is true, additionalProperties should be false.");
+
+ // Validate _NoAutoPK and _NoPK schemas as well
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas.ContainsKey("Book_NoAutoPK"),
+ "Schema 'Book_NoAutoPK' should exist in OpenAPI document.");
+ Assert.IsFalse(
+ openApiDocument.Components.Schemas["Book_NoAutoPK"].AdditionalPropertiesAllowed,
+ "When request-body-strict is true, Book_NoAutoPK additionalProperties should be false.");
+
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas.ContainsKey("Book_NoPK"),
+ "Schema 'Book_NoPK' should exist in OpenAPI document.");
+ Assert.IsFalse(
+ openApiDocument.Components.Schemas["Book_NoPK"].AdditionalPropertiesAllowed,
+ "When request-body-strict is true, Book_NoPK additionalProperties should be false.");
+ }
+
+ ///
+ /// Validates that when request-body-strict is false, the OpenAPI schema has
+ /// additionalProperties set to true, indicating that extra fields are allowed.
+ /// This addresses GitHub issue #2947 and #1838.
+ ///
+ [TestMethod]
+ public async Task OpenApiSchema_WhenRequestBodyStrictFalse_AdditionalPropertiesIsTrue()
+ {
+ // Arrange
+ Entity entity = new(
+ Source: new(Object: "books", EntitySourceType.Table, null, null),
+ Fields: null,
+ GraphQL: new(Singular: null, Plural: null, Enabled: false),
+ Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
+ Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
+ Mappings: null,
+ Relationships: null);
+
+ Dictionary entities = new()
+ {
+ { "Book", entity }
+ };
+
+ RuntimeEntities runtimeEntities = new(entities);
+
+ // request-body-strict: false (non-strict mode)
+ RestRuntimeOptions restOptions = new(Enabled: true, Path: "/api", RequestBodyStrict: false);
+
+ // Act - Create OpenApi document
+ OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync(
+ runtimeEntities: runtimeEntities,
+ configFileName: CUSTOM_CONFIG,
+ databaseEnvironment: MSSQL_ENVIRONMENT,
+ restOptions: restOptions);
+
+ // Assert - Validate that Book schema has additionalProperties set to true
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas.ContainsKey("Book"),
+ "Schema 'Book' should exist in OpenAPI document.");
+
+ OpenApiSchema bookSchema = openApiDocument.Components.Schemas["Book"];
+ Assert.IsTrue(
+ bookSchema.AdditionalPropertiesAllowed,
+ "When request-body-strict is false, additionalProperties should be true to indicate extra fields are allowed.");
+
+ // Validate _NoAutoPK and _NoPK schemas as well
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas.ContainsKey("Book_NoAutoPK"),
+ "Schema 'Book_NoAutoPK' should exist in OpenAPI document.");
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas["Book_NoAutoPK"].AdditionalPropertiesAllowed,
+ "When request-body-strict is false, Book_NoAutoPK additionalProperties should be true.");
+
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas.ContainsKey("Book_NoPK"),
+ "Schema 'Book_NoPK' should exist in OpenAPI document.");
+ Assert.IsTrue(
+ openApiDocument.Components.Schemas["Book_NoPK"].AdditionalPropertiesAllowed,
+ "When request-body-strict is false, Book_NoPK additionalProperties should be true.");
+ }
+ }
+}