Commit e14cca5
Fix duplicate entity groups in Swagger UI when entities have descriptions (#3099)
## Why make this change?
Swagger UI displays duplicate groups for entities with descriptions
(e.g., "Actor" appears twice). OpenAPI tags were instantiated separately
in `BuildOpenApiDocument` (global tags) and `BuildPaths` (operation
tags), causing Swagger UI to treat identical tag names as distinct
groups.
Additionally, a critical bug existed where REST paths with leading
slashes (e.g., `"path": "/Actor"`) would cause key mismatches between
`BuildOpenApiDocument` and `BuildPaths`, leading to duplicate tag
instances even with the shared dictionary approach.
## What is this change?
Share tag instances between global and operation-level tags, with
consistent REST path normalization. Applied on top of main's refactored
`BuildOpenApiDocument`/`BuildPaths` architecture:
**Before:**
```csharp
// BuildOpenApiDocument creates tag instance (no slash normalization)
List<OpenApiTag> globalTags = new();
string restPath = entity.Rest?.Path ?? kvp.Key; // Keeps "/Actor" with slash
globalTags.Add(new OpenApiTag { Name = restPath, Description = "..." });
// BuildPaths creates ANOTHER tag instance (with slash trimmed)
string entityRestPath = GetEntityRestPath(...); // Returns "Actor" without slash
OpenApiTag tag = new() { Name = entityRestPath, Description = "..." };
```
**After:**
```csharp
// BuildOpenApiDocument stores tags in dictionary with consistent path normalization
Dictionary<string, OpenApiTag> globalTagsDict = new();
string restPath = GetEntityRestPath(entity.Rest, kvp.Key); // Both use same normalization
globalTagsDict.TryAdd(restPath, new OpenApiTag { Name = restPath, Description = "..." });
// BuildPaths reuses the same instance (no fallback that silently reintroduces duplicates)
if (!globalTags.TryGetValue(entityRestPath, out OpenApiTag? existingTag))
{
_logger.LogWarning("Tag for REST path '{EntityRestPath}' not found in global tags dictionary...", entityRestPath);
continue;
}
tags.Add(existingTag); // Same object reference
```
**Changes:**
- `OpenApiDocumentor.BuildOpenApiDocument`:
- Store tags in `Dictionary<string, OpenApiTag>` keyed by REST path
- Use `GetEntityRestPath` for consistent path normalization (trims
leading slashes)
- Use `TryAdd` for cleaner deduplication
- First entity's description wins when multiple entities share the same
REST path
- Pass global tags dictionary to `BuildPaths` for instance reuse
- `OpenApiDocumentor.BuildPaths`:
- Accept `globalTags` dictionary parameter and reuse existing tag
instances
- Replaced silent fallback `else` block (which would reintroduce
duplicate tags) with `_logger.LogWarning` + `continue` to surface any
key mismatch immediately
- `TagValidationTests.cs`:
- Added comprehensive integration tests:
`NoDuplicateTags_WithDescription`,
`SharedTagInstances_BetweenGlobalAndOperationTags`,
`RestDisabledEntity_ProducesNoTag`, `RoleFilteredEntity_ProducesNoTag`
- Tests exercise leading-slash normalization, entity descriptions,
REST-disabled entities, and role-based filtering
- `StoredProcedureGeneration.cs`:
- Removed old `OpenApiDocumentor_NoDuplicateTags` test (moved to
`TagValidationTests`)
## How was this tested?
- [x] Integration Tests
- `NoDuplicateTags_WithDescription`: Verifies no duplicate tag names in
global tags when entities have descriptions
- `SharedTagInstances_BetweenGlobalAndOperationTags`: Verifies operation
tags reference same instances as global tags (via `ReferenceEquals`)
- `RestDisabledEntity_ProducesNoTag`: Verifies REST-disabled entities
produce no global tag
- `RoleFilteredEntity_ProducesNoTag`: Verifies entities filtered by role
produce no global tag
- [x] Unit Tests
## Sample Request(s)
N/A - This is an OpenAPI document generation fix. The Swagger UI at
`/api/openapi` will now display each entity group once instead of
duplicating them, even when REST paths are configured with leading
slashes.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*This section details on the original issue you should resolve*
<issue_title>[Bug]: Swagger UI shows duplicate entity groups for single
entity definition</issue_title>
<issue_description>When using Data API builder 1.7.75, the Swagger UI
generated at `/api/openapi` displays the `Actor` entity twice, even
though `Actor` is defined only once in the configuration. No other
entity shares its REST path or alias.
> I think this has to do with `descriptions`
<img width="934" height="1112" alt="Image"
src="https://github.com/user-attachments/assets/ed06124e-2550-431a-9155-ac2d4d875aca"
/>
**Steps to Reproduce:**
1. Use this configuration (trimmed for brevity):
```json
{
"entities": {
"Actor": {
"source": { "object": "dbo.Actor", "type": "table" },
"rest": { "enabled": true, "path": "/Actor" }
},
"Character": {
"source": { "object": "dbo.Character", "type": "table" },
"rest": { "enabled": true, "path": "/Character" }
},
"SeriesActors": {
"source": { "object": "dbo.SeriesActors", "type": "view" },
"rest": { "enabled": true, "path": "/SeriesActors" }
},
"GetSeriesActors": {
"source": { "object": "dbo.GetSeriesActors", "type": "stored-procedure"
},
"rest": { "enabled": true, "path": "/GetSeriesActors", "methods":
["post"] }
}
}
}
```
2. Start DAB (`dab start --config dab-config.json`).
3. Open the Swagger UI at `/api/openapi`.
**Expected Behavior:**
Each entity should appear once under its respective REST path group.
**Actual Behavior:**
Swagger shows two separate groups labeled `Actor`, each listing the same
endpoints (`GET`, `PUT`, `PATCH`, `DELETE`, `POST`).
**Environment:**
* Data API builder version: 1.7.75
* Database: SQL Server
* Host: Azure Container Apps
* Authentication: StaticWebApps provider
**Notes:**
* No other entity defines `path: "/Actor"`.
* The duplication persists even when unrelated entities (like stored
procedures or views) are removed.
* Suggest investigating the Swagger document generator—likely a
duplicate path or tag registration in `OpenApiDocumentBuilder`.
</issue_description>
<agent_instructions>Resolve this with as little new code as
possible</agent_instructions>
## Comments on the Issue (you are @copilot in this section)
<comments>
<comment_new><author>@JerryNixon</author><body>
### The Root Cause
The issue is that **tags are being added to the OpenAPI document in TWO
different places**, and when an entity has a `description` property, it
creates tags with **different object identities but the same name**.
### Where Tags Are Created
**1. Global Tags (in `CreateDocument` method):**
```csharp name=src/Core/Services/OpenAPI/OpenApiDocumentor.cs url=https://github.com/Azure/data-api-builder/blob/b11ab1a812d404ae8f176bc0cc4e483eac640133/src/Core/Services/OpenAPI/OpenApiDocumentor.cs#L145-L155
// Collect all entity tags and their descriptions for the top-level tags array
List<OpenApiTag> globalTags = new();
foreach (KeyValuePair<string, Entity> kvp in runtimeConfig.Entities)
{
Entity entity = kvp.Value;
string restPath = entity.Rest?.Path ?? kvp.Key;
globalTags.Add(new OpenApiTag
{
Name = restPath,
Description = string.IsNullOrWhiteSpace(entity.Description) ? null : entity.Description
});
}
OpenApiDocument doc = new()
{
// ...
Tags = globalTags // ← Global tags added here
};
```
**2. Per-Path Tags (in `BuildPaths` method):**
```csharp name=src/Core/Services/OpenAPI/OpenApiDocumentor.cs url=https://github.com/Azure/data-api-builder/blob/b11ab1a812d404ae8f176bc0cc4e483eac640133/src/Core/Services/OpenAPI/OpenApiDocumentor.cs#L229-L241
// Set the tag's Description property to the entity's semantic description if present.
OpenApiTag openApiTag = new()
{
Name = entityRestPath,
Description = string.IsNullOrWhiteSpace(entity.Description) ? null : entity.Description
};
// The OpenApiTag will categorize all paths created using the entity's name or overridden REST path value.
List<OpenApiTag> tags = new()
{
openApiTag // ← Per-path tag created here
};
```
These per-path tags are then assigned to each operation, and each
operation is added to the document's paths.
### Why This Causes Duplication
When Swagger UI renders the OpenAPI document, it looks at:
1. **Document-level tags** (`doc.Tags`) - Added in `CreateDocument`
2. **Operation-level tags** (each operation's `Tags` property) - Added
in `BuildPaths`
Even though both have the same `Name` ("Actor"), they are **different
object instances** with the same `Description`. Swagger UI treats them
as distinct tag definitions and displays them separately.
### Why You Noticed It with Descriptions
The user comment "I think this has to do with `descriptions`" is
correct! Here's why:
- **Without descriptions**: Both tag objects have `Description = null`,
so Swagger might merge them
- **With descriptions**: The OpenAPI s...
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes #2968
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
Co-authored-by: souvikghosh04 <210500244+souvikghosh04@users.noreply.github.com>
Co-authored-by: souvikghosh04 <souvikofficial04@gmail.com>1 parent fb4b2af commit e14cca5
2 files changed
Lines changed: 301 additions & 34 deletions
File tree
- src
- Core/Services/OpenAPI
- Service.Tests/OpenApiDocumentor
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
204 | | - | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
205 | 207 | | |
206 | 208 | | |
207 | 209 | | |
| |||
210 | 212 | | |
211 | 213 | | |
212 | 214 | | |
213 | | - | |
214 | | - | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
215 | 221 | | |
216 | 222 | | |
217 | 223 | | |
| |||
229 | 235 | | |
230 | 236 | | |
231 | 237 | | |
232 | | - | |
| 238 | + | |
233 | 239 | | |
234 | | - | |
| 240 | + | |
235 | 241 | | |
236 | 242 | | |
237 | 243 | | |
| |||
291 | 297 | | |
292 | 298 | | |
293 | 299 | | |
| 300 | + | |
294 | 301 | | |
295 | 302 | | |
296 | | - | |
| 303 | + | |
297 | 304 | | |
298 | 305 | | |
299 | 306 | | |
300 | 307 | | |
301 | 308 | | |
302 | 309 | | |
303 | 310 | | |
304 | | - | |
| 311 | + | |
305 | 312 | | |
306 | 313 | | |
307 | 314 | | |
308 | 315 | | |
309 | 316 | | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
314 | | - | |
315 | | - | |
316 | 317 | | |
317 | 318 | | |
318 | | - | |
319 | | - | |
320 | | - | |
321 | | - | |
322 | | - | |
323 | | - | |
324 | | - | |
325 | | - | |
| 319 | + | |
326 | 320 | | |
327 | 321 | | |
328 | 322 | | |
329 | 323 | | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
334 | | - | |
335 | | - | |
| 324 | + | |
| 325 | + | |
336 | 326 | | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
341 | | - | |
342 | | - | |
| 327 | + | |
| 328 | + | |
343 | 329 | | |
344 | 330 | | |
345 | 331 | | |
346 | | - | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
347 | 335 | | |
348 | 336 | | |
349 | 337 | | |
350 | 338 | | |
351 | 339 | | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
352 | 353 | | |
353 | 354 | | |
354 | 355 | | |
| |||
Lines changed: 266 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
0 commit comments