Departments V2 OpenAPI Drift Report
Summary
This report compares the documented v2 departments API against the generated OpenAPI contract in services/amm/agent-web-api/openapi.json.
For this report, docs/api/departments is the source of truth.
Scope Reviewed
/v2/clients/{client_id}/departments(GET, POST) inservices/amm/agent-web-api/openapi.json#L6014/v2/clients/{client_id}/departments/{department_id}(GET, PATCH, DELETE) inservices/amm/agent-web-api/openapi.json#L6235/v2/clients/{client_id}/departments/{department_id}/members(POST) inservices/amm/agent-web-api/openapi.json#L6503/v2/clients/{client_id}/departments/{department_id}/members/remove(POST) inservices/amm/agent-web-api/openapi.json#L6606DepartmentDto(list item schema)DepartmentDetailDto(detail/create/update response schema)DepartmentsListResponseDto(list wrapper)CreateDepartmentDto(create request body)UpdateDepartmentDto(update request body)MembersModifyDto(add/remove request body)MembersAddResponseDto(add members response)MembersRemoveResponseDto(remove members response)UserDepartmentRefDto(department reference embedded in user responses)
Findings
1. Member bulk response shape does not match the documented BulkMemberResult
This is the largest drift in the departments contract. The docs define a two-bucket response shape reused across both add and remove operations:
The generated OpenAPI uses a completely different three-bucket shape with different field names for each operation.
Documented response shape (BulkMemberResult)
{
"succeeded": [
"f1a2b3c4-d5e6-7890-abcd-ef1234567890"
],
"failed": [
{
"id": "ffffffff-ffff-ffff-ffff-ffffffffffff",
"error": "User not found"
}
]
}
Current generated contract shape — MembersAddResponseDto
{
"added": ["..."],
"already_members": ["..."],
"not_found": ["..."]
}
Current generated contract shape — MembersRemoveResponseDto
{
"removed": ["..."],
"not_members": ["..."],
"not_found": ["..."]
}
Drift details:
- Docs use
succeeded(string[]) andfailed(object[] withid+error); OpenAPI uses three flat string arrays with operation-specific names - The
failedarray in the docs carries per-ID error messages; the OpenAPI shape has no way to convey error reasons — it only categorizes IDs by failure type - The docs define a single shared
BulkMemberResulttype; the OpenAPI defines two separate DTOs (MembersAddResponseDto,MembersRemoveResponseDto)
2. DepartmentDetailDto is missing member_count
The docs define the Department object with a required member_count field. This field is used in the response for detail, create, and update:
/departments(index — DTO definition)/departments/details/departments/create(new departments havemember_count: 0)/departments/update
The generated DepartmentDetailDto does not include member_count (or members_count).
Documented Department shape
{
"id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"name": "Engineering",
"description": "The engineering department",
"member_count": 42,
"created_at": "2026-01-15T10:00:00.000Z",
"updated_at": "2026-02-20T14:30:00.000Z"
}
Current generated DepartmentDetailDto
{
"id": "...",
"client_id": "...",
"name": "...",
"description": null,
"created_at": "...",
"updated_at": "..."
}
This affects three endpoints that return DepartmentDetailDto:
GET /departments/{department_id}(200)POST /departments(201)PATCH /departments/{department_id}(200)
3. Field name mismatch: member_count (docs) vs members_count (OpenAPI)
The docs consistently use member_count (singular). The generated DepartmentDto (list item) uses members_count (plural).
/departments— definesmember_count/departments/list— lists it in response
Documented field name
member_count (integer, required)
Current generated field name
members_count (number)
This also cascades to the sort_by enum:
- Docs:
sort_byvalues arename,member_count,created_at - OpenAPI:
sort_byenum isname,members_count,created_at
4. description typed as nullable object instead of nullable string in request DTOs
The docs define description as string | null everywhere. Several generated schemas mistype it as nullable object:
CreateDepartmentDto.description— typed as nullableobjectUpdateDepartmentDto.description— typed as nullableobjectUserDepartmentRefDto.description— typed as nullableobject
Note: DepartmentDto.description and DepartmentDetailDto.description are correctly typed as nullable string.
Documented type
description (string, nullable)
Current generated type in CreateDepartmentDto
{
"description": {
"type": "object",
"description": "Optional description",
"nullable": true
}
}
5. DepartmentDto and DepartmentDetailDto include undocumented client_id field
Both generated DTOs include client_id as a required field. The docs do not include client_id in the Department object shape.
Documented fields
id, name, description, member_count, created_at, updated_at
Current generated required fields (DepartmentDto)
id, client_id, name, description, members_count, created_at, updated_at
This is not necessarily breaking — it's extra data — but it diverges from the documented contract and exposes an internal identifier that the docs intentionally omit.
6. Missing 404 responses on list and create endpoints
The docs say these endpoints return 404 when client_id does not resolve:
/departments/list— documents404/departments/create— documents404
The generated OpenAPI does not document 404 on those routes:
GET /v2/clients/{client_id}/departments— no404responsePOST /v2/clients/{client_id}/departments— no404response
Other department endpoints (detail, update, delete, members add, members remove) correctly document 404.
Documented status set for GET /departments
200, 400, 401, 403, 404, 500
Current generated status set for GET /departments
200, 400, 401, 403, 500
7. page_size typed as number instead of integer
The docs define page_size as an integer. The generated OpenAPI types it as number.
Documented type
page_size (integer, default: 50, max: 200)
Current generated type
{
"type": "number",
"maximum": 200,
"default": 50
}
While JavaScript doesn't distinguish integer from number at runtime, the OpenAPI spec convention is to use type: "integer" for whole-number pagination parameters. Code generators in typed languages (Go, Java, C#) will produce float/double instead of int.
Notes
Route coverage is fully aligned
All seven documented department routes are present in the generated OpenAPI. No documented route is missing, and no undocumented route exists.
List response wrapper is aligned
DepartmentsListResponseDto correctly includes data (array), total_count (number), and next_cursor (nullable string), matching the documented shape.
Error response structure is aligned
DepartmentErrorResponseDto wraps DepartmentErrorDto with code, message, and nullable details — matching the documented error shapes.
Delete endpoint is aligned
DELETE /departments/{department_id} correctly returns 204 No Content with the right status code set (204, 401, 403, 404, 500).
Severity Summary
| # | Finding | Severity | Endpoints Affected |
|---|---|---|---|
| 1 | Bulk member response shape mismatch | High | POST members, POST members/remove |
| 2 | DepartmentDetailDto missing member_count | High | GET detail, POST create, PATCH update |
| 3 | member_count vs members_count naming | Medium | GET list, sort_by param |
| 4 | description typed as object not string | Medium | POST create, PATCH update, UserDepartmentRefDto |
| 5 | Undocumented client_id field | Low | All department responses |
| 6 | Missing 404 on list and create | Low | GET list, POST create |
| 7 | page_size typed as number not integer | Low | GET list |
Requested Action
Please update the NestJS DTOs, decorators, and controller response metadata so that the generated OpenAPI matches the documented v2 departments API in docs/api/departments, then regenerate services/amm/agent-web-api/openapi.json and validate the contract.