Skip to content

Commit 245ed3b

Browse files
CopilotJerryNixon
andcommitted
Implement OpenAPI request-body-strict support (#2947, #1838)
Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
1 parent a8ae56a commit 245ed3b

3 files changed

Lines changed: 199 additions & 15 deletions

File tree

src/Core/Services/OpenAPI/OpenApiDocumentor.cs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public void CreateDocument(bool doOverrideExistingDocument = false)
135135
string url = string.IsNullOrEmpty(runtimeBaseRoute) ? restEndpointPath : runtimeBaseRoute + "/" + restEndpointPath;
136136
OpenApiComponents components = new()
137137
{
138-
Schemas = CreateComponentSchemas(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName)
138+
Schemas = CreateComponentSchemas(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName, runtimeConfig.IsRequestBodyStrict)
139139
};
140140

141141
// Collect all entity tags and their descriptions for the top-level tags array
@@ -976,9 +976,14 @@ private static OpenApiMediaType CreateResponseContainer(string responseObjectSch
976976
/// 2) {EntityName}_NoAutoPK -> No auto-generated primary keys present in schema, used for POST requests where PK is not autogenerated and GET (all).
977977
/// 3) {EntityName}_NoPK -> No primary keys present in schema, used for POST requests where PK is autogenerated and GET (all).
978978
/// Schema objects can be referenced elsewhere in the OpenAPI document with the intent to reduce document verbosity.
979+
/// When isRequestBodyStrict is false, request body schemas will have additionalProperties set to true
980+
/// to indicate that extra fields are allowed and will be ignored.
979981
/// </summary>
982+
/// <param name="entities">Runtime entities from configuration.</param>
983+
/// <param name="defaultDataSourceName">Default data source name.</param>
984+
/// <param name="isRequestBodyStrict">Whether extra fields are rejected in request bodies.</param>
980985
/// <returns>Collection of schemas for entities defined in the runtime configuration.</returns>
981-
private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities entities, string defaultDataSourceName)
986+
private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities entities, string defaultDataSourceName, bool isRequestBodyStrict)
982987
{
983988
Dictionary<string, OpenApiSchema> schemas = new();
984989
// for rest scenario we need the default datasource name.
@@ -1006,18 +1011,20 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
10061011
if (dbObject.SourceType is EntitySourceType.StoredProcedure)
10071012
{
10081013
// Request body schema whose properties map to stored procedure parameters
1014+
// When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
10091015
DatabaseStoredProcedure spObject = (DatabaseStoredProcedure)dbObject;
1010-
schemas.Add(entityName + SP_REQUEST_SUFFIX, CreateSpRequestComponentSchema(fields: spObject.StoredProcedureDefinition.Parameters));
1016+
schemas.Add(entityName + SP_REQUEST_SUFFIX, CreateSpRequestComponentSchema(fields: spObject.StoredProcedureDefinition.Parameters, allowAdditionalProperties: !isRequestBodyStrict));
10111017

10121018
// Response body schema whose properties map to the stored procedure's first result set columns
10131019
// as described by sys.dm_exec_describe_first_result_set.
1014-
schemas.Add(entityName + SP_RESPONSE_SUFFIX, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
1020+
schemas.Add(entityName + SP_RESPONSE_SUFFIX, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: false));
10151021
}
10161022
else
10171023
{
10181024
// Create component schema for FULL entity with all primary key columns (included auto-generated)
10191025
// which will typically represent the response body of a request or a stored procedure's request body.
1020-
schemas.Add(entityName, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
1026+
// When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
1027+
schemas.Add(entityName, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: !isRequestBodyStrict));
10211028

10221029
// Create an entity's request body component schema excluding autogenerated primary keys.
10231030
// A POST request requires any non-autogenerated primary key references to be in the request body.
@@ -1037,7 +1044,8 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
10371044
}
10381045
}
10391046

1040-
schemas.Add($"{entityName}_NoAutoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
1047+
// When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
1048+
schemas.Add($"{entityName}_NoAutoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: !isRequestBodyStrict));
10411049

10421050
// Create an entity's request body component schema excluding all primary keys
10431051
// by removing the tracked non-autogenerated primary key column names and removing them from
@@ -1053,7 +1061,8 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
10531061
}
10541062
}
10551063

1056-
schemas.Add($"{entityName}_NoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities));
1064+
// When isRequestBodyStrict is false, additionalProperties is set to true to indicate extra fields are allowed.
1065+
schemas.Add($"{entityName}_NoPK", CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, allowAdditionalProperties: !isRequestBodyStrict));
10571066
}
10581067
}
10591068

@@ -1068,8 +1077,9 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
10681077
/// </summary>
10691078
/// </summary>
10701079
/// <param name="fields">Collection of stored procedure parameter metadata.</param>
1080+
/// <param name="allowAdditionalProperties">When true, sets additionalProperties to true indicating extra fields are allowed.</param>
10711081
/// <returns>OpenApiSchema object representing a stored procedure's request body.</returns>
1072-
private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary<string, ParameterDefinition> fields)
1082+
private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary<string, ParameterDefinition> fields, bool allowAdditionalProperties)
10731083
{
10741084
Dictionary<string, OpenApiSchema> properties = new();
10751085
HashSet<string> required = new();
@@ -1097,7 +1107,8 @@ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary<string, P
10971107
{
10981108
Type = SCHEMA_OBJECT_TYPE,
10991109
Properties = properties,
1100-
Required = required
1110+
Required = required,
1111+
AdditionalPropertiesAllowed = allowAdditionalProperties
11011112
};
11021113

11031114
return schema;
@@ -1115,10 +1126,11 @@ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary<string, P
11151126
/// <param name="fields">List of mapped (alias) field names.</param>
11161127
/// <param name="metadataProvider">Metadata provider for database objects.</param>
11171128
/// <param name="entities">Runtime entities from configuration.</param>
1129+
/// <param name="allowAdditionalProperties">When true, sets additionalProperties to true indicating extra fields are allowed.</param>
11181130
/// <exception cref="DataApiBuilderException">Raised when an entity's database metadata can't be found,
11191131
/// indicating a failure due to the provided entityName.</exception>
11201132
/// <returns>Entity's OpenApiSchema representation.</returns>
1121-
private static OpenApiSchema CreateComponentSchema(string entityName, HashSet<string> fields, ISqlMetadataProvider metadataProvider, RuntimeEntities entities)
1133+
private static OpenApiSchema CreateComponentSchema(string entityName, HashSet<string> fields, ISqlMetadataProvider metadataProvider, RuntimeEntities entities, bool allowAdditionalProperties)
11221134
{
11231135
if (!metadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null)
11241136
{
@@ -1166,7 +1178,8 @@ private static OpenApiSchema CreateComponentSchema(string entityName, HashSet<st
11661178
{
11671179
Type = SCHEMA_OBJECT_TYPE,
11681180
Properties = properties,
1169-
Description = entityConfig?.Description
1181+
Description = entityConfig?.Description,
1182+
AdditionalPropertiesAllowed = allowAdditionalProperties
11701183
};
11711184

11721185
return schema;

src/Service.Tests/OpenApiDocumentor/OpenApiTestBootstrap.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,43 @@ internal static async Task<OpenApiDocument> GenerateOpenApiDocumentAsync(
3232
RuntimeEntities runtimeEntities,
3333
string configFileName,
3434
string databaseEnvironment)
35+
{
36+
return await GenerateOpenApiDocumentAsync(
37+
runtimeEntities: runtimeEntities,
38+
configFileName: configFileName,
39+
databaseEnvironment: databaseEnvironment,
40+
restOptions: null);
41+
}
42+
43+
/// <summary>
44+
/// Bootstraps a test server instance using a runtime config file generated
45+
/// from the provided entity collection and REST options. The test server is only used to generate
46+
/// and return the OpenApiDocument for use this method's callers.
47+
/// </summary>
48+
/// <param name="runtimeEntities"></param>
49+
/// <param name="configFileName"></param>
50+
/// <param name="databaseEnvironment"></param>
51+
/// <param name="restOptions">Optional REST runtime options to customize request-body-strict setting.</param>
52+
/// <returns>Generated OpenApiDocument</returns>
53+
internal static async Task<OpenApiDocument> GenerateOpenApiDocumentAsync(
54+
RuntimeEntities runtimeEntities,
55+
string configFileName,
56+
string databaseEnvironment,
57+
RestRuntimeOptions restOptions)
3558
{
3659
TestHelper.SetupDatabaseEnvironment(databaseEnvironment);
3760
FileSystem fileSystem = new();
3861
FileSystemRuntimeConfigLoader loader = new(fileSystem);
3962
loader.TryLoadKnownConfig(out RuntimeConfig config);
4063

64+
// Create custom REST options if provided, otherwise use existing config
65+
RuntimeOptions runtimeWithRestOptions = restOptions is not null
66+
? config.Runtime with { Rest = restOptions, Host = config.Runtime?.Host with { Mode = HostMode.Production } }
67+
: config.Runtime with { Host = config.Runtime?.Host with { Mode = HostMode.Production } };
68+
4169
RuntimeConfig configWithCustomHostMode = config with
4270
{
43-
Runtime = config.Runtime with
44-
{
45-
Host = config.Runtime?.Host with { Mode = HostMode.Production }
46-
},
71+
Runtime = runtimeWithRestOptions,
4772
Entities = runtimeEntities
4873
};
4974

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Azure.DataApiBuilder.Config.ObjectModel;
8+
using Microsoft.OpenApi.Models;
9+
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
11+
namespace Azure.DataApiBuilder.Service.Tests.OpenApiIntegration
12+
{
13+
/// <summary>
14+
/// Integration tests validating that OpenAPI document schemas respect the request-body-strict configuration.
15+
/// When request-body-strict is false, schemas should have additionalProperties set to true to indicate
16+
/// that extra fields in request bodies are allowed and will be ignored.
17+
/// </summary>
18+
[TestCategory(TestCategory.MSSQL)]
19+
[TestClass]
20+
public class RequestBodyStrictTests
21+
{
22+
private const string CUSTOM_CONFIG = "request-body-strict.MsSql.json";
23+
private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL;
24+
25+
/// <summary>
26+
/// Validates that when request-body-strict is true (default), the OpenAPI schema has
27+
/// additionalProperties set to false, indicating that extra fields are not allowed.
28+
/// </summary>
29+
[TestMethod]
30+
public async Task OpenApiSchema_WhenRequestBodyStrictTrue_AdditionalPropertiesIsFalse()
31+
{
32+
// Arrange
33+
Entity entity = new(
34+
Source: new(Object: "books", EntitySourceType.Table, null, null),
35+
Fields: null,
36+
GraphQL: new(Singular: null, Plural: null, Enabled: false),
37+
Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
38+
Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
39+
Mappings: null,
40+
Relationships: null);
41+
42+
Dictionary<string, Entity> entities = new()
43+
{
44+
{ "Book", entity }
45+
};
46+
47+
RuntimeEntities runtimeEntities = new(entities);
48+
49+
// request-body-strict: true (default)
50+
RestRuntimeOptions restOptions = new(Enabled: true, Path: "/api", RequestBodyStrict: true);
51+
52+
// Act - Create OpenApi document
53+
OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync(
54+
runtimeEntities: runtimeEntities,
55+
configFileName: CUSTOM_CONFIG,
56+
databaseEnvironment: MSSQL_ENVIRONMENT,
57+
restOptions: restOptions);
58+
59+
// Assert - Validate that Book schema has additionalProperties set to false
60+
Assert.IsTrue(
61+
openApiDocument.Components.Schemas.ContainsKey("Book"),
62+
"Schema 'Book' should exist in OpenAPI document.");
63+
64+
OpenApiSchema bookSchema = openApiDocument.Components.Schemas["Book"];
65+
Assert.IsFalse(
66+
bookSchema.AdditionalPropertiesAllowed,
67+
"When request-body-strict is true, additionalProperties should be false.");
68+
69+
// Validate _NoAutoPK and _NoPK schemas as well
70+
Assert.IsTrue(
71+
openApiDocument.Components.Schemas.ContainsKey("Book_NoAutoPK"),
72+
"Schema 'Book_NoAutoPK' should exist in OpenAPI document.");
73+
Assert.IsFalse(
74+
openApiDocument.Components.Schemas["Book_NoAutoPK"].AdditionalPropertiesAllowed,
75+
"When request-body-strict is true, Book_NoAutoPK additionalProperties should be false.");
76+
77+
Assert.IsTrue(
78+
openApiDocument.Components.Schemas.ContainsKey("Book_NoPK"),
79+
"Schema 'Book_NoPK' should exist in OpenAPI document.");
80+
Assert.IsFalse(
81+
openApiDocument.Components.Schemas["Book_NoPK"].AdditionalPropertiesAllowed,
82+
"When request-body-strict is true, Book_NoPK additionalProperties should be false.");
83+
}
84+
85+
/// <summary>
86+
/// Validates that when request-body-strict is false, the OpenAPI schema has
87+
/// additionalProperties set to true, indicating that extra fields are allowed.
88+
/// This addresses GitHub issue #2947 and #1838.
89+
/// </summary>
90+
[TestMethod]
91+
public async Task OpenApiSchema_WhenRequestBodyStrictFalse_AdditionalPropertiesIsTrue()
92+
{
93+
// Arrange
94+
Entity entity = new(
95+
Source: new(Object: "books", EntitySourceType.Table, null, null),
96+
Fields: null,
97+
GraphQL: new(Singular: null, Plural: null, Enabled: false),
98+
Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
99+
Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
100+
Mappings: null,
101+
Relationships: null);
102+
103+
Dictionary<string, Entity> entities = new()
104+
{
105+
{ "Book", entity }
106+
};
107+
108+
RuntimeEntities runtimeEntities = new(entities);
109+
110+
// request-body-strict: false (non-strict mode)
111+
RestRuntimeOptions restOptions = new(Enabled: true, Path: "/api", RequestBodyStrict: false);
112+
113+
// Act - Create OpenApi document
114+
OpenApiDocument openApiDocument = await OpenApiTestBootstrap.GenerateOpenApiDocumentAsync(
115+
runtimeEntities: runtimeEntities,
116+
configFileName: CUSTOM_CONFIG,
117+
databaseEnvironment: MSSQL_ENVIRONMENT,
118+
restOptions: restOptions);
119+
120+
// Assert - Validate that Book schema has additionalProperties set to true
121+
Assert.IsTrue(
122+
openApiDocument.Components.Schemas.ContainsKey("Book"),
123+
"Schema 'Book' should exist in OpenAPI document.");
124+
125+
OpenApiSchema bookSchema = openApiDocument.Components.Schemas["Book"];
126+
Assert.IsTrue(
127+
bookSchema.AdditionalPropertiesAllowed,
128+
"When request-body-strict is false, additionalProperties should be true to indicate extra fields are allowed.");
129+
130+
// Validate _NoAutoPK and _NoPK schemas as well
131+
Assert.IsTrue(
132+
openApiDocument.Components.Schemas.ContainsKey("Book_NoAutoPK"),
133+
"Schema 'Book_NoAutoPK' should exist in OpenAPI document.");
134+
Assert.IsTrue(
135+
openApiDocument.Components.Schemas["Book_NoAutoPK"].AdditionalPropertiesAllowed,
136+
"When request-body-strict is false, Book_NoAutoPK additionalProperties should be true.");
137+
138+
Assert.IsTrue(
139+
openApiDocument.Components.Schemas.ContainsKey("Book_NoPK"),
140+
"Schema 'Book_NoPK' should exist in OpenAPI document.");
141+
Assert.IsTrue(
142+
openApiDocument.Components.Schemas["Book_NoPK"].AdditionalPropertiesAllowed,
143+
"When request-body-strict is false, Book_NoPK additionalProperties should be true.");
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)