Skip to main content

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) in services/amm/agent-web-api/openapi.json#L6014
  • /v2/clients/{client_id}/departments/{department_id} (GET, PATCH, DELETE) in services/amm/agent-web-api/openapi.json#L6235
  • /v2/clients/{client_id}/departments/{department_id}/members (POST) in services/amm/agent-web-api/openapi.json#L6503
  • /v2/clients/{client_id}/departments/{department_id}/members/remove (POST) in services/amm/agent-web-api/openapi.json#L6606
  • DepartmentDto (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[]) and failed (object[] with id + error); OpenAPI uses three flat string arrays with operation-specific names
  • The failed array 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 BulkMemberResult type; 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:

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).

Documented field name

member_count (integer, required)

Current generated field name

members_count (number)

This also cascades to the sort_by enum:

  • Docs: sort_by values are name, member_count, created_at
  • OpenAPI: sort_by enum is name, 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 nullable object
  • UpdateDepartmentDto.description — typed as nullable object
  • UserDepartmentRefDto.description — typed as nullable object

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:

The generated OpenAPI does not document 404 on those routes:

  • GET /v2/clients/{client_id}/departments — no 404 response
  • POST /v2/clients/{client_id}/departments — no 404 response

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

#FindingSeverityEndpoints Affected
1Bulk member response shape mismatchHighPOST members, POST members/remove
2DepartmentDetailDto missing member_countHighGET detail, POST create, PATCH update
3member_count vs members_count namingMediumGET list, sort_by param
4description typed as object not stringMediumPOST create, PATCH update, UserDepartmentRefDto
5Undocumented client_id fieldLowAll department responses
6Missing 404 on list and createLowGET list, POST create
7page_size typed as number not integerLowGET 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.