The @rocket.chat/rest-typings package uses two AJV instances for JSON schema validation: ajv and ajvQuery. The choice between them depends on the source of the data being validated (request body vs query string).
Both are created in packages/rest-typings/src/v1/Ajv.ts with the same configuration except for one option:
| Option | ajv |
ajvQuery |
|---|---|---|
coerceTypes |
false |
true |
allowUnionTypes |
true |
true |
code.source |
true |
true |
discriminator |
true |
true |
In short:
ajv: does not change data types; values must already match the types expected by the schema.ajvQuery: attempts to coerce types when the schema expectsnumber,integer, orboolean(e.g. the string"50"becomes the number50).
Custom formats (addFormats) and keywords (e.g. isNotEmpty) are registered on both instances.
In HTTP requests, query parameters (everything after ? in the URL) reach the server as strings. HTTP does not carry type information; the server receives, for example:
?count=25→countis the string"25"?open=true→openis the string"true"
If the schema expects count as number or open as boolean, a validator that does not coerce will reject:
"25"is not of typenumber→ error like "must be number" / "invalid-params"."true"is not of typeboolean→ validation error.
For the body (JSON in POST/PUT/PATCH etc.), the client sends JSON. Parsing (e.g. JSON.parse) already yields numbers and booleans. In that case we do not want the validator to mutate values; we use the instance without coercion (ajv).
- The validator is used for query parameters (query string).
- GET routes (or any method that only reads query params).
- The schema has properties of type
number,integer, orbooleanthat come from the URL.
Examples of validators that should use ajvQuery:
- Pagination:
count,offset(typically numbers). - Flags:
open,readThreads(booleans). - Any numeric or boolean parameter the client sends in the query string.
// GET /v1/livechat/rooms?count=25&offset=0
export const isGETLivechatRoomsParams = ajvQuery.compile<GETLivechatRoomsParams>(GETLivechatRoomsParamsSchema);- The validator is used for the request body (POST, PUT, PATCH, etc.).
- Data is already parsed JSON (numbers and booleans are already typed).
Examples:
- Create/update resources (JSON body).
- Response validators or internal structures that do not come from the query string.
// POST /v1/livechat/room/close — JSON body
export const isPOSTLivechatRoomCloseParams = ajv.compile<POSTLivechatRoomCloseParams>(...);| Data source | Instance | Reason |
|---|---|---|
| Query string (GET, query params) | ajvQuery |
Query values are strings; coerceTypes: true converts to number/boolean when the schema expects it. |
| Body (JSON in POST/PUT/PATCH) | ajv |
JSON already has types; strict validation without mutating values. |
Response schemas also use ajv (coerceTypes: false). In test mode, the Router validates every response against its declared schema (options.response[statusCode]). If validation fails, the Router returns a 400 with errorType: "error-invalid-body" instead of the original response.
With coerceTypes: true (old behavior), null values were silently coerced (e.g. null → "" for strings). With coerceTypes: false, any field that can be null must declare nullable: true in the schema — otherwise the response validator rejects it.
A video conference user object may have avatarETag: null. The response schema must account for this:
// WRONG — fails when avatarETag is null
{ type: 'string' }
// CORRECT
{ type: 'string', nullable: true }Schemas using oneOf with strict enum discriminators (e.g. type: { enum: ['direct'] }) also become stricter without coercion. If the actual data has a type value not listed in any branch, the oneOf fails. Ensure all possible discriminator values are covered, or relax the items schema (e.g. { type: 'object' }) when full type-level validation is not needed at runtime.
-
Using
ajvfor query params when the schema expectsnumberorboolean:- Client sends
?count=25. - Validator expects
numberbut receives the string"25". - Result: "must be number" / "invalid-params" error.
- Client sends
-
Using
ajvQueryfor body:- Usually works, but coercion can hide incorrect types (e.g. string where a number was expected). For body, the standard is to use
ajvand require the client to send the correct types in the JSON.
- Usually works, but coercion can hide incorrect types (e.g. string where a number was expected). For body, the standard is to use
-
Response schemas with
nullfields (test mode only):- With
coerceTypes: false,nullis no longer coerced to""or0. - Fields that can be
nullmust usenullable: true. - Symptom: tests get 400 with
errorType: "error-invalid-body"even though the endpoint logic succeeds.
- With
- File:
packages/rest-typings/src/v1/Ajv.ts - Export:
export { ajv, ajvQuery }; - Usage: In each rest-typings module, import the appropriate instance and call
.compile(schema)to obtain the validator used by the API routes.