Skip to content

Commit c860fc5

Browse files
aaronburtleCopilotAniruddh25anushakolan
authored
Omit redundant _NoAutoPK and _NoPK OpenAPI schemas when request-body-strict is false (#3325)
## Why make this change? Closes #3260 ## What is this change? When request-body-strict is set to false, the OpenAPI document was still generating separate _NoAutoPK and _NoPK component schemas for each entity. These schemas only differ from the base entity schema by excluding primary key fields, a distinction that is meaningless when the request body already allows additional properties. This made the generated OpenAPI document unnecessarily verbose and confusing for consumers. The `isRequestBodyStrict` flag is now threaded through `BuildPaths` and `CreateOperations` so that POST/PUT/PATCH request body schema references point to the base entity schema when strict mode is off. The `CreateComponentSchemas` method now gates `_NoAutoPK` and `_NoPK` schema generation on strict mode, and its XML documentation has been updated to accurately describe both strict and non-strict behavior. ## How was this tested? - [ ] Integration Tests - [x] Unit Tests Updated existing unit test `RequestBodyStrict_False_OmitsRedundantSchemas` (renamed from `RequestBodyStrict_False_AllowsExtraFields`) to verify that when request-body-strict is false: - `_NoAutoPK` and `_NoPK` component schemas are not generated. - The base entity schema is still present. - POST/PUT/PATCH operations reference the base entity schema (not `_NoAutoPK` or `_NoPK`). --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh Munde <anmunde@microsoft.com> Co-authored-by: Anusha Kolan <anushakolan10@gmail.com>
1 parent cc6c638 commit c860fc5

2 files changed

Lines changed: 69 additions & 23 deletions

File tree

src/Core/Services/OpenAPI/OpenApiDocumentor.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ private OpenApiDocument BuildOpenApiDocument(RuntimeConfig runtimeConfig, string
235235
{
236236
new() { Url = url }
237237
},
238-
Paths = BuildPaths(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName, globalTagsDict, role),
238+
Paths = BuildPaths(runtimeConfig.Entities, runtimeConfig.DefaultDataSourceName, globalTagsDict, role, isRequestBodyStrict: runtimeConfig.IsRequestBodyStrict),
239239
Components = components,
240240
Tags = globalTagsDict.Values.ToList()
241241
};
@@ -300,7 +300,7 @@ public void CreateDocument(bool doOverrideExistingDocument = false)
300300
/// <param name="globalTags">Dictionary of global tags keyed by normalized REST path for reuse.</param>
301301
/// <param name="role">Optional role to filter permissions. If null, returns superset of all roles.</param>
302302
/// <returns>All possible paths in the DAB engine's REST API endpoint.</returns>
303-
private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSourceName, Dictionary<string, OpenApiTag> globalTags, string? role = null)
303+
private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSourceName, Dictionary<string, OpenApiTag> globalTags, string? role = null, bool isRequestBodyStrict = true)
304304
{
305305
OpenApiPaths pathsCollection = new();
306306

@@ -377,7 +377,8 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
377377
sourceDefinition: sourceDefinition,
378378
includePrimaryKeyPathComponent: true,
379379
configuredRestOperations: configuredRestOperations,
380-
tags: tags);
380+
tags: tags,
381+
isRequestBodyStrict: isRequestBodyStrict);
381382

382383
if (pkOperations.Count > 0)
383384
{
@@ -400,7 +401,8 @@ private OpenApiPaths BuildPaths(RuntimeEntities entities, string defaultDataSour
400401
sourceDefinition: sourceDefinition,
401402
includePrimaryKeyPathComponent: false,
402403
configuredRestOperations: configuredRestOperations,
403-
tags: tags);
404+
tags: tags,
405+
isRequestBodyStrict: isRequestBodyStrict);
404406

405407
if (operations.Count > 0)
406408
{
@@ -435,7 +437,8 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
435437
SourceDefinition sourceDefinition,
436438
bool includePrimaryKeyPathComponent,
437439
Dictionary<OperationType, bool> configuredRestOperations,
438-
List<OpenApiTag> tags)
440+
List<OpenApiTag> tags,
441+
bool isRequestBodyStrict = true)
439442
{
440443
Dictionary<OperationType, OpenApiOperation> openApiPathItemOperations = new();
441444

@@ -457,7 +460,8 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
457460
if (configuredRestOperations[OperationType.Put])
458461
{
459462
OpenApiOperation putOperation = CreateBaseOperation(description: PUT_DESCRIPTION, tags: tags);
460-
putOperation.RequestBody = CreateOpenApiRequestBodyPayload($"{entityName}_NoPK", requestBodyRequired);
463+
string putPatchSchemaRef = isRequestBodyStrict ? $"{entityName}_NoPK" : entityName;
464+
putOperation.RequestBody = CreateOpenApiRequestBodyPayload(putPatchSchemaRef, requestBodyRequired);
461465
putOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName));
462466
putOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: entityName));
463467
openApiPathItemOperations.Add(OperationType.Put, putOperation);
@@ -466,7 +470,8 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
466470
if (configuredRestOperations[OperationType.Patch])
467471
{
468472
OpenApiOperation patchOperation = CreateBaseOperation(description: PATCH_DESCRIPTION, tags: tags);
469-
patchOperation.RequestBody = CreateOpenApiRequestBodyPayload($"{entityName}_NoPK", requestBodyRequired);
473+
string patchSchemaRef = isRequestBodyStrict ? $"{entityName}_NoPK" : entityName;
474+
patchOperation.RequestBody = CreateOpenApiRequestBodyPayload(patchSchemaRef, requestBodyRequired);
470475
patchOperation.Responses.Add(HttpStatusCode.OK.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.OK), responseObjectSchemaName: entityName));
471476
patchOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: entityName));
472477
openApiPathItemOperations.Add(OperationType.Patch, patchOperation);
@@ -496,7 +501,7 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
496501

497502
if (configuredRestOperations[OperationType.Post])
498503
{
499-
string postBodySchemaReferenceId = DoesSourceContainAutogeneratedPrimaryKey(sourceDefinition) ? $"{entityName}_NoAutoPK" : $"{entityName}";
504+
string postBodySchemaReferenceId = isRequestBodyStrict && DoesSourceContainAutogeneratedPrimaryKey(sourceDefinition) ? $"{entityName}_NoAutoPK" : $"{entityName}";
500505
OpenApiOperation postOperation = CreateBaseOperation(description: POST_DESCRIPTION, tags: tags);
501506
postOperation.RequestBody = CreateOpenApiRequestBodyPayload(postBodySchemaReferenceId, IsRequestBodyRequired(sourceDefinition, considerPrimaryKeys: true));
502507
postOperation.Responses.Add(HttpStatusCode.Created.ToString("D"), CreateOpenApiResponse(description: nameof(HttpStatusCode.Created), responseObjectSchemaName: entityName));
@@ -509,7 +514,7 @@ private Dictionary<OperationType, OpenApiOperation> CreateOperations(
509514
// which is useful for entities with identity/auto-generated keys.
510515
if (DoesSourceContainAutogeneratedPrimaryKey(sourceDefinition))
511516
{
512-
string keylessBodySchemaReferenceId = $"{entityName}_NoAutoPK";
517+
string keylessBodySchemaReferenceId = isRequestBodyStrict ? $"{entityName}_NoAutoPK" : entityName;
513518
bool keylessRequestBodyRequired = IsRequestBodyRequired(sourceDefinition, considerPrimaryKeys: true);
514519

515520
if (configuredRestOperations[OperationType.Put])
@@ -1276,14 +1281,16 @@ private static OpenApiMediaType CreateResponseContainer(string responseObjectSch
12761281

12771282
/// <summary>
12781283
/// Builds the schema objects for all entities present in the runtime configuration.
1279-
/// Two schemas per entity are created:
1280-
/// 1) {EntityName} -> Primary keys present in schema, used for request bodies (excluding GET) and all response bodies.
1281-
/// 2) {EntityName}_NoAutoPK -> No auto-generated primary keys present in schema, used for POST requests where PK is not autogenerated and GET (all).
1282-
/// 3) {EntityName}_NoPK -> No primary keys present in schema, used for POST requests where PK is autogenerated and GET (all).
1284+
/// When isRequestBodyStrict is true, up to three schemas per entity are created:
1285+
/// 1) {EntityName} -> All columns including primary keys. Used for response bodies and as request body when strict mode is off.
1286+
/// 2) {EntityName}_NoAutoPK -> Excludes auto-generated primary keys. Used for POST request bodies (strict mode only).
1287+
/// 3) {EntityName}_NoPK -> Excludes all primary keys. Used for PUT/PATCH request bodies (strict mode only).
1288+
/// When isRequestBodyStrict is false, only the base {EntityName} schema is created and all
1289+
/// request body operations reference it directly, since extra properties are allowed.
12831290
/// Schema objects can be referenced elsewhere in the OpenAPI document with the intent to reduce document verbosity.
12841291
/// </summary>
12851292
/// <param name="role">Optional role to filter permissions. If null, returns superset of all roles.</param>
1286-
/// <param name="isRequestBodyStrict">When true, request body schemas disallow extra fields.</param>
1293+
/// <param name="isRequestBodyStrict">When true, generates separate request body schemas that disallow extra fields.</param>
12871294
/// <returns>Collection of schemas for entities defined in the runtime configuration.</returns>
12881295
private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities entities, string defaultDataSourceName, string? role = null, bool isRequestBodyStrict = true)
12891296
{
@@ -1342,7 +1349,7 @@ private Dictionary<string, OpenApiSchema> CreateComponentSchemas(RuntimeEntities
13421349
schemas.Add(entityName, CreateComponentSchema(entityName, fields: exposedColumnNames, metadataProvider, entities, isRequestBodySchema: false));
13431350

13441351
// Only generate request body schemas if mutation operations are available
1345-
if (hasPostOperation || hasPutPatchOperation)
1352+
if (isRequestBodyStrict && (hasPostOperation || hasPutPatchOperation))
13461353
{
13471354
// Create an entity's request body component schema excluding autogenerated primary keys.
13481355
// A POST request requires any non-autogenerated primary key references to be in the request body.

src/Service.Tests/OpenApiDocumentor/RequestBodyStrictTests.cs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,61 @@ public async Task RequestBodyStrict_True_DisallowsExtraFields()
4343
}
4444

4545
/// <summary>
46-
/// Validates that when request-body-strict is false, request body schemas
47-
/// have additionalProperties set to true.
46+
/// Validates that when request-body-strict is false, the redundant _NoAutoPK and _NoPK
47+
/// schemas are not generated. Operations reference the base entity schema instead.
4848
/// </summary>
4949
[TestMethod]
50-
public async Task RequestBodyStrict_False_AllowsExtraFields()
50+
public async Task RequestBodyStrict_False_OmitsRedundantSchemas()
5151
{
5252
OpenApiDocument doc = await GenerateDocumentWithPermissions(
5353
OpenApiTestBootstrap.CreateBasicPermissions(),
5454
requestBodyStrict: false);
5555

56-
// Request body schemas should have additionalProperties = true
57-
Assert.IsTrue(doc.Components.Schemas.ContainsKey("book_NoAutoPK"), "POST request body schema should exist");
58-
Assert.IsTrue(doc.Components.Schemas["book_NoAutoPK"].AdditionalPropertiesAllowed, "POST request body should allow extra fields in non-strict mode");
56+
// _NoAutoPK and _NoPK schemas should not be generated when strict mode is off
57+
Assert.IsFalse(doc.Components.Schemas.ContainsKey("book_NoAutoPK"), "POST request body schema should not exist in non-strict mode");
58+
Assert.IsFalse(doc.Components.Schemas.ContainsKey("book_NoPK"), "PUT/PATCH request body schema should not exist in non-strict mode");
5959

60-
Assert.IsTrue(doc.Components.Schemas.ContainsKey("book_NoPK"), "PUT/PATCH request body schema should exist");
61-
Assert.IsTrue(doc.Components.Schemas["book_NoPK"].AdditionalPropertiesAllowed, "PUT/PATCH request body should allow extra fields in non-strict mode");
60+
// Base entity schema should still exist
61+
Assert.IsTrue(doc.Components.Schemas.ContainsKey("book"), "Base entity schema should exist");
62+
63+
// Operations (POST/PUT/PATCH) should reference the base 'book' schema for their request bodies
64+
bool foundRequestBodyForWritableOperation = false;
65+
foreach (OpenApiPathItem pathItem in doc.Paths.Values)
66+
{
67+
foreach (KeyValuePair<OperationType, OpenApiOperation> operationKvp in pathItem.Operations)
68+
{
69+
OperationType operationType = operationKvp.Key;
70+
OpenApiOperation operation = operationKvp.Value;
71+
72+
if (operationType != OperationType.Post
73+
&& operationType != OperationType.Put
74+
&& operationType != OperationType.Patch)
75+
{
76+
continue;
77+
}
78+
79+
if (operation.RequestBody is null)
80+
{
81+
continue;
82+
}
83+
84+
if (!operation.RequestBody.Content.TryGetValue("application/json", out OpenApiMediaType mediaType)
85+
|| mediaType.Schema is null)
86+
{
87+
continue;
88+
}
89+
90+
foundRequestBodyForWritableOperation = true;
91+
OpenApiSchema schema = mediaType.Schema;
92+
93+
Assert.IsNotNull(schema.Reference, "Request body schema should reference a component schema when request-body-strict is false.");
94+
Assert.AreEqual("book", schema.Reference.Id, "Request body should reference the base 'book' schema when request-body-strict is false.");
95+
Assert.AreNotEqual("book_NoAutoPK", schema.Reference.Id, "Request body should not reference the 'book_NoAutoPK' schema when request-body-strict is false.");
96+
Assert.AreNotEqual("book_NoPK", schema.Reference.Id, "Request body should not reference the 'book_NoPK' schema when request-body-strict is false.");
97+
}
98+
}
99+
100+
Assert.IsTrue(foundRequestBodyForWritableOperation, "Expected at least one POST/PUT/PATCH operation with a JSON request body.");
62101
}
63102

64103
private static async Task<OpenApiDocument> GenerateDocumentWithPermissions(EntityPermission[] permissions, bool? requestBodyStrict = null)

0 commit comments

Comments
 (0)