6.3 JSON templates (structural, injection-safe substitution)
Most query languages you'll target — the reference adapter, Mongo aggregates, Elastic DSL — are JSON. RS2 substitutes parameters into a JSON query structurally, which makes it valid JSON at rest and injection-safe by construction.
Structural substitution
A string node that is exactly "${name}" is replaced by the parameter's
JSON value — not by string interpolation. So a number stays a number, an array
stays an array:
"where": { "total": { "op": ">=", "value": "${min}" } }
If min is 20, the result is { "op": ">=", "value": 20 } — the integer 20,
not the string "20". Because the param's typed value is dropped in as a JSON
node, there's no string concatenation anywhere, and therefore no way to inject
query syntax through a parameter.
And because "${min}" is itself a valid JSON string, the template is valid
JSON even before substitution — you can store, lint, and diff it like any JSON
document.
Optional placeholders: "${name?}"
A trailing ? marks an optional placeholder. If the param is absent, the
enclosing clause is elided — the object member or array element it sits in
is removed entirely:
"where": { "status": "${status}", "name": "${name?}" }
- With
namesupplied →{ "status": "open", "name": "Ada" }. - With
nameabsent →{ "status": "open" }(the wholenamemember drops).
This generalizes the common "ignore empty filters" need into a language-agnostic rule: optional filters simply disappear when their input isn't given, so one stored query covers "filter by name" and "don't" without branching.
Placeholders inside longer strings
A placeholder embedded in a longer string ("name:${who}", or a positional
"$0" spliced into text) is substituted as text, adapter-quoted — the
adapter is responsible for escaping it for its own syntax. The injection-safe
guarantee is strongest for the exact-match "${name}" form (structural); prefer
it where you can.
Dynamic object keys
An object key can itself be an exact placeholder. The parameter must resolve to a string; an optional missing key removes the member:
{ "$sort": { "${sortField?}": "${sortDir?}" } }
This is useful for query languages such as Mongo's aggregation syntax, where a
field name is part of the instruction rather than a value. A non-string key is
rejected with 400 rather than being guessed.
Gating a whole clause with $if
Put "$if": "${flag?}" on an object to include or remove that whole object. It
is removed when the flag is absent or empty (null, false, "", or []);
otherwise $if is stripped and the remaining object is substituted normally:
[
{ "$if": "${filterByStatus?}",
"$match": { "status": "${status}" } },
{ "$sort": { "created": -1 } }
]
That keeps optional pipeline stages declarative instead of forcing several near-identical stored queries.
Why this is the safe default
You author a JSON query, parameters arrive typed and validated (6.2), and they're dropped into the query as JSON nodes. There's no SQL-string-building step where an attacker could smuggle in syntax. For the cases that genuinely need string queries (real SQL with bind parameters), the next section explains how those are handled differently — and just as safely.
← Previous: 6.2 Parameters · Manual home · Next: 6.4 String/SQL templates →