docs/ajv-instances.md
ajv and ajvQueryThe @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 expects number, integer, or boolean (e.g. the string "50" becomes the number 50).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 → count is the string "25"?open=true → open is 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 type number → error like "must be number" / "invalid-params"."true" is not of type boolean → 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).
ajvQuery.compile when:number, integer, or boolean that come from the URL.Examples of validators that should use ajvQuery:
count, offset (typically numbers).open, readThreads (booleans).// GET /v1/livechat/rooms?count=25&offset=0
export const isGETLivechatRoomsParams = ajvQuery.compile<GETLivechatRoomsParams>(GETLivechatRoomsParamsSchema);
ajv.compile when:Examples:
// 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. |
nullableResponse 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 }
oneOf / discriminator schemasSchemas 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 ajv for query params when the schema expects number or boolean:
?count=25.number but receives the string "25".Using ajvQuery for body:
ajv and require the client to send the correct types in the JSON.Response schemas with null fields (test mode only):
coerceTypes: false, null is no longer coerced to "" or 0.null must use nullable: true.errorType: "error-invalid-body" even though the endpoint logic succeeds.packages/rest-typings/src/v1/Ajv.tsexport { ajv, ajvQuery };.compile(schema) to obtain the validator used by the API routes.