Skip to main content
Version: v7 / v8

Export

Refer to the TypeScript reference page for information about the types and interfaces referenced below.

Use the formatQuery function to export queries in various formats. The function has this signature:

function formatQuery(
query: RuleGroupTypeAny,
options?: ExportFormat | FormatQueryOptions
): string | ParameterizedSQL | ParameterizedNamedSQL | RQBJsonLogic | Record<string, any>;

formatQuery converts query objects to these formats:

  • Formatted JSON.stringify result
  • Unformatted JSON.stringify result with all id and path properties removed
  • SQL WHERE clause
    • Parameterized with anonymous parameters
    • Parameterized with named parameters
  • ORM query objects for Drizzle, Prisma, Sequelize, and TanStack DB
  • MongoDB query object
  • MongoDB query object as string (deprecated)
  • Common Expression Language (CEL)
  • Spring Expression Language (SpEL)
  • JsonLogic
  • ElasticSearch
  • Vector database metadata filters (Pinecone, Chroma, Qdrant, Weaviate, Milvus)
  • JSONata
  • LDAP
  • Natural language

The following sections use this example query:

const query: RuleGroupType = {
id: 'root',
combinator: 'and',
not: false,
rules: [
{
id: 'rule1',
field: 'firstName',
operator: '=',
value: 'Steve',
},
{
id: 'rule2',
field: 'lastName',
operator: '=',
value: 'Vai',
},
],
};
tip

For best results, use default combinators and operators or map custom ones to defaults with transformQuery.

More information...

formatQuery accepts RuleGroupTypeAny queries but only guarantees correct processing of DefaultRuleGroupTypeAny queries.

All query combinator and operator properties must match defaultCombinators or defaultOperators names (case-insensitive). Use transformQuery to map custom names to defaults before calling formatQuery.

For example, replacing the default "between" operator with { name: "b/w", label: "b/w" } creates rules with operator: "b/w". For this query:

{
"combinator": "and",
"rules": [{ "field": "someNumber", "operator": "b/w", "value": "12,14" }]
}

Transform it using transformQuery with operatorMap:

const newQuery = transformQuery(query, { operatorMap: { 'b/w': 'between' } });
/*
{
"combinator": "and",
"rules": [{ "field": "someNumber", "operator": "between", "value": "12,14" }]
}
*/

The newQuery is ready for formatQuery, including special "between" operator handling.

Basic usage​

JSON​

Export the internal query representation (from onQueryChange callback) as formatted JSON:

formatQuery(query);
// or
formatQuery(query, 'json');

Output is multi-line JSON with 2-space indentation:

`{
"id": "root",
"combinator": "and",
"not": false,
"rules": [
{
"id": "rule1",
"field": "firstName",
"value": "Steve",
"operator": "="
},
{
"id": "rule2",
"field": "lastName",
"value": "Vai",
"operator": "="
}
]
}`;

JSON without IDs​

Export unformatted (single-line) JSON without id or path attributes using "json_without_ids". This format is useful for persistent storage:

formatQuery(query, 'json_without_ids');

Output (string):

{"combinator":"and","not":false,"rules":[{"field":"firstName","value":"Steve","operator":"="},{"field":"lastName","value":"Vai","operator":"="}]}

SQL​

Export SQL WHERE clauses using the "sql" format. This format is compatible with major RDBMS engines, though some cases require configuration. See presets for compatibility details.

formatQuery(query, 'sql');

Output (string):

(firstName = 'Steve' and lastName = 'Vai')

Parameterized SQL​

Export SQL with bind variables instead of inline values using the "parameterized" format. This returns an object with sql and params properties:

formatQuery(query, 'parameterized');

Output (JSON object):

{
"sql": "(firstName = ? and lastName = ?)",
"params": ["Steve", "Vai"]
}

Named parameters​

When anonymous parameters aren't suitable, use "parameterized_named" to name parameters based on field names. This is similar to "parameterized" but params is an object instead of an array:

formatQuery(query, 'parameterized_named');

Output (JSON object):

{
"sql": "(firstName = :firstName_1 and lastName = :lastName_1)",
"params": {
"firstName_1": "Steve",
"lastName_1": "Vai"
}
}

See also: paramPrefix and generating parameter names.

ORMs​

Prisma ORM​

Generate objects for Prisma ORM where properties using the "prisma" format:

Note: Prisma does not support field-to-field comparisons, so rules with valueSource: "field" will always be invalid.

const where = formatQuery(query, 'prisma');

console.log(where);
// { AND: [{ firstName: 'Steve' }, { lastName: 'Vai' }] }

const users = await prisma.users.findMany({ where });

Drizzle ORM​

Relational Queries API​

Generate functions for Drizzle's relational queries API where property:

const where = formatQuery(query, 'drizzle');
// typeof where === 'function'
// where.length === 2
const results = db.query.users.findMany({ where });
Query Builder API​

For Drizzle's query builder API, pass table definition and operators to the formatQuery-generated function:

import { getOperators } from 'drizzle-orm';

const whereFn = formatQuery(query, 'drizzle');
const whereObj = whereFn(table, getOperators());

const query = db.select().from(table).where(whereObj);
tip

Query builder API objects work with other Drizzle operators, letting you add conditions not in the original query:

import { and, ne, getOperators } from 'drizzle-orm';

// Conditions from the React Query Builder query object:
const whereFn = formatQuery(query, 'drizzle');
const whereObj = whereFn(table, getOperators());

// All conditions from the original query object _and_ `id != 123`:
const augmentedWhere = and(whereObj, ne(table.id, 123));
const query = db.select().from(table).where(augmentedWhere);
@react-querybuilder/drizzle (deprecated)

The @react-querybuilder/drizzle package previously provided generateDrizzleRuleGroupProcessor and generateDrizzleRuleProcessor for integration with Drizzle's query builder API. This package is now deprecated.

To achieve the same result, inline the following functions in your project:

import type { RuleGroupProcessor, RuleProcessor } from '@react-querybuilder/core';
import {
defaultRuleGroupProcessorDrizzle,
defaultRuleProcessorDrizzle,
} from '@react-querybuilder/core';
import type { Column, SQL, Table } from 'drizzle-orm';
import * as drizzleOperators from 'drizzle-orm';
import { getOperators } from 'drizzle-orm';

export const generateDrizzleRuleGroupProcessor =
(columns: Record<string, Column> | Table): RuleGroupProcessor<SQL | undefined> =>
(ruleGroup, options) =>
defaultRuleGroupProcessorDrizzle(ruleGroup, options)(
columns as Record<string, Column>,
getOperators()
);

export const generateDrizzleRuleProcessor =
(table: Table | Record<string, Column>): RuleProcessor =>
(rule, options) =>
defaultRuleProcessorDrizzle(rule, { ...options, context: { table, drizzleOperators } });

Usage:

import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { formatQuery } from 'react-querybuilder';

const db = drizzle(process.env.DB_FILE_NAME!);

const table = sqliteTable('musicians', {
firstName: text(),
lastName: text(),
});

const ruleGroupProcessor = generateDrizzleRuleGroupProcessor(table);

// Tip: `format` is not required when `ruleGroupProcessor` is provided
const where = formatQuery(query, { ruleGroupProcessor });

const query = db.select().from(table).where(where);
console.log(query.toSQL());
// {
// sql: 'select "firstName", "lastName" from "musicians" where ("musicians"."firstName" = ? and "musicians"."lastName" = ?)',
// params: ['Steve', 'Vai']
// }

console.log(query.all());
// [{ firstName: 'Steve', lastName: 'Vai' }]

Sequelize​

Generate objects for Sequelize findAll where properties using the "sequelize" format. Requirements:

  • Sequelize uses Symbols for operator keys, so they must be provided through the context option as sequelizeOperators (see example below).
  • If any rules have valueSource: "field", then the Sequelize col function must be provided as sequelizeCol.
  • If any rules have valueSource: "field" and use one of the doesNot* operators, then the Sequelize fn function must be provided as sequelizeFn.
import { col, fn, Op } from 'sequelize';
const where = formatQuery(query, {
format: 'sequelize',
context: { sequelizeOperators: Op, sequelizeCol: col, sequelizeFn: fn },
});

const users = await Users.findAll({ where });

TanStack DB​

Generate a WhereCallback for TanStack DB's .where() method using the "tanstack_db" format. The processor does not import any executable code from @tanstack/db — operators are passed in through the context option.

Pass the full @tanstack/db module or individual operators as tanStackDbOperators:

import * as tsdb from '@tanstack/db';

const where = formatQuery(query, {
format: 'tanstack_db',
context: { tanStackDbOperators: tsdb },
});

const results = useLiveQuery(q => q.from({ users: usersCollection }).where(where));

Or with cherry-picked operators:

import { eq, gt, gte, lt, lte, like, inArray, isNull, not, and, or } from '@tanstack/db';

const where = formatQuery(query, {
format: 'tanstack_db',
context: {
tanStackDbOperators: { eq, gt, gte, lt, lte, like, inArray, isNull, not, and, or },
},
});
tip

TanStack DB does not expose ne, between, notBetween, notInArray, notLike, or isNotNull — these are composed automatically using not(...). For example, != becomes not(eq(...)) and between becomes and(gte(...), lte(...)).

Joins (multi-collection queries)​

When querying across joined collections, fields from non-primary collections must use dotted notation ("alias.fieldName") to target the correct ref. Bare (unprefixed) fields always resolve to the primary collection (the first key in the refs object).

import * as tsdb from '@tanstack/db';

const query = {
combinator: 'and',
rules: [
// Bare field → resolves to the primary collection (su)
{ field: 'firstName', operator: '=', value: 'Bruce' },
// Dotted field → resolves to the nicknames collection (nn)
{ field: 'nn.nickname', operator: 'contains', value: 'Dark' },
],
};

const where = formatQuery(query, {
format: 'tanstack_db',
context: { tanStackDbOperators: tsdb },
});

const results = useLiveQuery(q =>
q
.from({ su: superUsersCollection })
.leftJoin({ nn: nicknamesCollection }, refs => eq(refs.su.id, refs.nn.userId))
.where(where)
);
caution

Bare fields cannot be disambiguated across collections at export time because TanStack DB refs are proxies that accept any property name. Always use dotted notation for fields on joined (non-primary) collections.

MongoDB​

Generate MongoDB queries as JSON objects or strings. Use the "mongodb_query" format (recommended) for JSON objects. The "mongodb" format is the stringified version.

info

The "mongodb" format was deprecated when the "mongodb_query" export format was introduced in version 8.1.0.

formatQuery(query, 'mongodb_query');

Output (JSON object):

{ "$and": [{ "firstName": "Steve" }, { "lastName": "Vai" }] }

Common Expression Language​

For Common Expression Language (CEL) output, use the "cel" format.

formatQuery(query, 'cel');

Output (string):

firstName = "Steve" && lastName = "Vai"

Spring Expression Language​

For Spring Expression Language (SpEL) output, use the "spel" format.

formatQuery(query, 'spel');

Output (string):

firstName == 'Steve' and lastName == 'Vai'

JsonLogic​

Generate objects for JsonLogic apply function (see https://jsonlogic.com/):

formatQuery(query, 'jsonlogic');

Output (JSON object):

{ "and": [{ "==": [{ "var": "firstName" }, "Steve"] }, { "==": [{ "var": "lastName" }, "Vai"] }] }
tip

Register additional startsWith and endsWith operators from react-querybuilder before using JsonLogic's apply(). These aren't standard JsonLogic operations but correspond to "beginsWith" and "endsWith" operators.

Loop through jsonLogicAdditionalOperators entries for future-proof registration of any new custom operators:

import { add_operation, apply } from 'json-logic-js';
import { jsonLogicAdditionalOperators } from 'react-querybuilder';

for (const [op, func] of Object.entries(jsonLogicAdditionalOperators)) {
add_operation(op, func);
}

apply({ startsWith: [{ var: 'firstName' }, 'Stev'] }, data);

ElasticSearch​

Generate objects for ElasticSearch processing:

formatQuery(query, 'elasticsearch');

Output (JSON object):

{ "bool": { "must": [{ "term": { "firstName": "Steve" } }, { "term": { "lastName": "Vai" } }] } }

Vector databases​

Experimental

Support for vector database formats is experimental.

The "pinecone", "chroma", "qdrant", "weaviate", and "milvus" formats generate metadata filters for vector searches (e.g. RAG pipelines). Rules a format can't express are skipped. Inspect formatCapabilities[format] at runtime (e.g. to limit operators per target), or run the "diagnostics" format with diagnosticsTargetFormat to list skipped rules as warnings.

FeaturePineconeChromaQdrantWeaviateMilvus
=✓✓✓✓✓
!=✓✓✓✓✓
<✓✓✓✓✓
>✓✓✓✓✓
<=✓✓✓✓✓
>=✓✓✓✓✓
contains––✓✓✓
beginsWith––✓✓✓
endsWith–––✓✓
doesNotContain––✓✓✓
doesNotBeginWith––✓✓✓
doesNotEndWith–––✓✓
null✓–✓✓✓
notNull✓–✓✓✓
in✓✓✓✓✓
notIn✓✓✓✓✓
between✓✓✓✓✓
notBetween✓✓✓✓✓
valueSource: "field"––––Only =, !=, <, >, <=, >=, between, notBetween
Match modes––some, none––
not groupsDe MorganDe Morgannativenativenative

Common behavior:

  • Object formats return {} when no rules can be expressed (or the query is invalid); omit the filter param in that case. "milvus" returns "".
  • "pinecone" and "chroma" have no group-level negation, so not groups are rewritten with De Morgan's laws (e.g. NOT (a = 1 AND b > 2) → a != 1 OR b <= 2). Records missing a field may match differently than with a true NOT.
  • Field names aren't escaped. Object formats emit them as plain keys/values (inert data), and "milvus" emits them verbatim. Whitelist field names either way.
  • bigint values are converted to number (precision loss beyond Number.MAX_SAFE_INTEGER), except in "milvus" which emits them as-is.
  • Fields with valueEditorType "checkbox"/"switch" emit boolean values.
  • Dates: "qdrant" and "weaviate" emit RFC 3339 strings for date/datetime-local fields. This requires field metadata via the fields option (e.g. formatQuery(query, { format: "qdrant", fields: [{ name: "createdAt", label: "Created", inputType: "date" }] })); without it, Qdrant skips non-numeric range rules and Weaviate emits valueText instead of valueDate. "pinecone", "chroma", and "milvus" have no date type; store dates as epoch milliseconds and use the datetimeRuleProcessor<Format> processors from @react-querybuilder/datetime, which convert date values (including relative dates) to epoch ms.

Pinecone​

Generate Pinecone metadata filters:

formatQuery(query, 'pinecone');

Output (JSON object):

{ "$and": [{ "firstName": { "$eq": "Steve" } }, { "lastName": { "$eq": "Vai" } }] }
  • Pass the result as the filter param of a query.
  • null/notNull → $exists: false/$exists: true.
  • Range operators (incl. between) require numeric values; others are skipped. in/notIn accept strings and numbers only.
  • String operators, valueSource: "field", and match modes are skipped.

Chroma​

Generate Chroma where filters:

formatQuery(query, 'chroma');

Output (JSON object):

{ "$and": [{ "firstName": { "$eq": "Steve" } }, { "lastName": { "$eq": "Vai" } }] }
  • Pass the result as the where param. Chroma rejects where: {}, so omit the param when the result is empty.
  • Each object holds one field and one operator, so between becomes $and of $gte/$lte. Single-child groups are unwrapped.
  • in/notIn values must share one type (string, integer, float, or boolean); mixed lists are skipped.
  • null/notNull are skipped (Chroma has no $exists). String operators are skipped; full-text where_document filters are out of scope.

Qdrant​

Generate Qdrant Filter objects for metadata filtering in vector searches (e.g. RAG pipelines):

formatQuery(query, 'qdrant');

Output (JSON object):

{
"must": [
{ "key": "firstName", "match": { "value": "Steve" } },
{ "key": "lastName", "match": { "value": "Vai" } }
]
}

Pass the result as the filter param of a search/query/scroll request. An empty object ({}) is returned when no rules can be expressed; omit the filter param in that case.

Rules Qdrant can't express are skipped. Inspect formatCapabilities.qdrant at runtime, or run the "diagnostics" format with diagnosticsTargetFormat: 'qdrant' to list skipped rules as warnings.

Operator(s)Output
= / !=match.value (must_not for !=); floats use range: { gte, lte }
< <= > >= / between notBetweenrange (numbers, or RFC 3339 strings for date/datetime-local)
in / notInmatch.any / match.except (all strings or all integers), else should
null / notNullis_null / must_not is_null
contains / doesNotContainmatch.text (requires full-text index; token-based, not substring)
beginsWith / doesNotBeginWithmatch.prefix (Qdrant v1.19+; case-sensitive, untokenized, keywords)
endsWith / doesNotEndWithskipped
valueSource: "field"skipped
match modes some / nonenested / must_not nested (arrays of objects only); others skipped
groupsand → must, or → should, not → must_not

Notes:

  • Field names are emitted verbatim as key (Qdrant supports a.b and a[].b paths). No escaping is applied, so whitelist field names.
  • bigint values are converted to number (precision loss beyond Number.MAX_SAFE_INTEGER).
  • Fields with valueEditorType "checkbox"/"switch" emit boolean values.

Weaviate​

Generate Weaviate where filters (GraphQL/REST shape):

formatQuery(query, 'weaviate');

Output (JSON object):

{
"operator": "And",
"operands": [
{ "operator": "Equal", "path": ["firstName"], "valueText": "Steve" },
{ "operator": "Equal", "path": ["lastName"], "valueText": "Vai" }
]
}
Operator(s)Output
= != < <= > >=Equal, NotEqual, LessThan, …
contains / beginsWith / endsWithLike with *v* / v* / *v; negations wrap Like in Not
in / notInContainsAny / ContainsNone (array under the singular value key)
between / notBetweenAnd(GreaterThanEqual, LessThanEqual) / Or(LessThan, GreaterThan)
null / notNullIsNull with valueBoolean: true / false
groupsAnd / Or / Not; single-operand groups unwrapped
  • The value key comes from the field: date/datetime-local → valueDate (RFC 3339), checkbox/switch → valueBoolean, numbers (number/range/bigint input types or parseNumbers) → valueInt/valueNumber, otherwise valueText.
  • path is [field]. Set fieldIdentifierSeparator to split cross-reference paths (e.g. "inPublication.Publication.name" with "." → ["inPublication", "Publication", "name"]).
  • * and ? in Like values are wildcards. Weaviate has no escape syntax, so they pass through unchanged.
  • IsNull requires invertedIndexConfig.indexNullState: true on the collection.
  • Not and ContainsNone require Weaviate v1.33 or later.
  • For GraphQL queries, serialize the object as a GraphQL input literal (unquoted keys, operator values as enums).

Milvus​

Generate Milvus boolean filter expressions:

formatQuery(query, 'milvus');

Output:

firstName == "Steve" and lastName == "Vai"
  • Pass the result as the filter/expr param. An empty string means no filter.
  • Strings are double-quoted with \ and " escaped. String operators use like with %, _, and \ in values escaped; negations use not (...).
  • null/notNull emit is null/is not null, which require Milvus 2.5 or later. On older versions, omit those operators.
  • valueSource: "field" is supported for comparison operators and between/notBetween (e.g. a > b). Milvus doesn't support field-to-field comparisons on JSON or ARRAY fields.
  • Field names are emitted verbatim, including JSON paths like meta["k"]. Whitelist them.

pgvector and LanceDB​

These don't need a dedicated format:

  • pgvector: use "parameterized" with preset: 'postgresql' and add the vector ordering yourself, e.g. SELECT * FROM docs WHERE ${sql} ORDER BY embedding <-> $${params.length + 1} LIMIT 10.
  • LanceDB: pass "sql" output (DataFusion SQL) to .where(). Quote field names that need it with quoteFieldNamesWith: '"'.

JSONata​

Generate JSONata filters using "jsonata" format. Use parseNumbers option for numeric values since JSONata doesn't auto-cast strings to numbers:

formatQuery(query, { format: 'jsonata', parseNumbers: true });

Output (string):

firstName = "Steve" and lastName = "Vai"
Handling date values in JSONata

React Query Builder lacks standard date detection, so use datetimeRuleProcessorJSONata from @react-querybuilder/datetime.

For more control, implement a custom rule processor (example below lacks error checking but provides a starting point):

const customRuleProcessor: RuleProcessor = (rule, options) => {
// `datatype` is a non-standard property of the field, used for this example only.
// Replace this condition with your own logic to determine if the value is a date.
if (options?.fieldData?.datatype === 'date') {
return `$toMillis(${rule.field}) ${rule.operator} $toMillis("${rule.value}")`;
}
return defaultRuleProcessorJSONata(rule, options);
};

LDAP​

Generate LDAP filters:

Note: LDAP filters do not support direct comparison between the values of two attributes within the same entry, so rules with valueSource: "field" will always be invalid.

formatQuery(query, 'ldap');

Output (string):

(&(givenName=Steve)(sn=Vai))

Natural language​

Generate natural language queries using "natural_language" format. Use getOperators and fields options to render labels instead of values. See i18n options:

formatQuery(query, {
format: 'natural_language',
parseNumbers: true,
getOperators: () => defaultOperators,
fields: [
{ value: 'firstName', label: 'First Name' },
{ value: 'lastName', label: 'Last Name' },
{ value: 'age', label: 'Age' },
],
});

Output (string):

First Name is 'Steve', and Last Name is "Vai", and Age is between 26 and 52

Cypher​

Generate Cypher WHERE clause conditions using the "cypher" format. This format is also available as "gql" since GQL uses the same expression syntax.

formatQuery(query, 'cypher');
// or
formatQuery(query, 'gql');

Output (string):

n.firstName = 'Steve' AND n.lastName = 'Vai'

SPARQL​

Generate SPARQL FILTER expressions using the "sparql" format:

formatQuery(query, 'sparql');

Output (string):

?firstName = "Steve" && ?lastName = "Vai"

Gremlin​

Generate Apache TinkerPop Gremlin .has() steps using the "gremlin" format:

formatQuery(query, 'gremlin');

Output (string):

.has('firstName', 'Steve').has('lastName', 'Vai')

react-awesome-query-builder​

Generate a react-awesome-query-builder (RAQB) query tree in its plain-JSON form, suitable for RAQB's Utils.loadTree().

This is not a built-in export format. Since most projects need it exactly once, it lives in the separate @react-querybuilder/migrate-raqb package, which exports a formatRAQB function.

npm i @react-querybuilder/migrate-raqb
import { Utils } from '@react-awesome-query-builder/core';
import { formatRAQB } from '@react-querybuilder/migrate-raqb';

const jsonTree = formatRAQB(query, { fields });
const immutableTree = Utils.checkTree(Utils.loadTree(jsonTree), config);

This is the inverse of parseRAQB. See Migrating from react-awesome-query-builder for the full concept mapping.

Output (object):

{
"type": "group",
"properties": { "conjunction": "AND", "not": false },
"children1": [
{
"type": "rule",
"properties": {
"field": "firstName",
"operator": "equal",
"value": ["Steve"],
"valueSrc": ["value"]
}
},
{
"type": "rule",
"properties": {
"field": "lastName",
"operator": "equal",
"value": ["Vai"],
"valueSrc": ["value"]
}
}
]
}

A companion function, formatRAQBFields, converts an RQB fields array to the fields section of an RAQB Config:

import { formatRAQBFields } from '@react-querybuilder/migrate-raqb';

const config = { ...BasicConfig, fields: formatRAQBFields(fields) };

formatRAQB accepts all formatQuery options except format, ruleGroupProcessor, and fallbackExpression, plus the RAQB-specific options below and fallbackTree (the tree returned when the query is empty or fails validation).

const jsonTree = formatRAQB(query, { fields, raqbFieldSeparator: '.', raqbValueTypes: true });
OptionDefaultDescription
raqbOperatorMap{}Additional/overriding RQB-to-RAQB operator mappings, keyed by RQB operator name.
raqbFunctionMap{}Additional/overriding expression-function-to-RAQB function name mappings.
raqbFuncArgOrder{}Argument names per RAQB function name. Defaults cover RAQB's built-ins; others get arg0, arg1, ...
raqbFieldSeparator"."Separator used to qualify sub-query rule fields with their parent !group field name.
raqbRelativeDateTimestrueConvert relative date/time values to RAQB's built-in date/time functions.
raqbValueTypesfalseEmit valueType entries inferred from each field's inputType/valueEditorType.

To combine RAQB output with other formatQuery behavior, or to supply a custom ruleProcessor, use the underlying defaultRuleGroupProcessorRAQB with the ruleGroupProcessor option, which takes precedence over format. RAQB options then move into context, and raqbFallback should be passed as fallbackExpression if a validator can invalidate the whole query.

import { defaultRuleGroupProcessorRAQB, raqbFallback } from '@react-querybuilder/migrate-raqb';

const jsonTree = formatQuery(query, {
ruleGroupProcessor: defaultRuleGroupProcessorRAQB,
fields,
context: { raqbValueTypes: true },
fallbackExpression: raqbFallback as unknown as string,
});
note

RAQB's default configuration has no equivalent for RQB's doesNotBeginWith/doesNotEndWith operators, the "parameter" value source, or endOf* relative date/time anchors. Rules using them are omitted (or, for endOf* anchors, emitted as plain values). Use raqbOperatorMap to map them onto custom RAQB operators.

RAQB also restricts some constructs that RQB permits—notably in/notIn on non-select fields and most field-to-field comparisons. See Migrating from react-awesome-query-builder for the full list.

Diagnostics​

Generate a diagnostics result object using the "diagnostics" format. The output includes an annotated copy of the query tree, a flat diagnostics array, aggregate statistics, and a per-field summary.

const result = formatQuery(query, {
format: 'diagnostics',
fields: [
{
name: 'firstName',
label: 'First Name',
validator: r => (r.value ? true : { valid: false, reasons: ['Value is required'] }),
},
{ name: 'age', label: 'Age', inputType: 'number' },
],
});

Output (object):

{
"query": {
"combinator": "and",
"valid": false,
"path": [],
"level": 0,
"rules": [
{
"field": "firstName",
"operator": "=",
"value": "",
"valid": false,
"reasons": ["Value is required"],
"path": [0],
"level": 1
},
{
"field": "age",
"operator": ">",
"value": 26,
"valid": true,
"path": [1],
"level": 1
}
]
},
"diagnostics": [
{
"id": "r-1",
"path": [0],
"code": "CUSTOM_VALIDATOR",
"message": "Invalid: Value is required",
"source": "field-validator"
}
],
"stats": {
"totalRules": 2,
"totalGroups": 1,
"validRules": 1,
"invalidRules": 1,
"validGroups": 0,
"invalidGroups": 1
},
"fieldSummary": {
"firstName": { "ruleCount": 1, "invalidCount": 1 },
"age": { "ruleCount": 1, "invalidCount": 0 }
}
}

Annotated query tree​

Every rule and group in result.query includes:

  • valid — whether the node passed all checks
  • reasons — optional array of reasons (from validators)
  • path — the position of the node in the tree (e.g., [1, 0])
  • level — the nesting depth (path.length)

A rule is considered invalid if any of the following are true:

  • The rule is muted
  • The rule fails validation via the validator option or a field-level validator
  • The field, operator, or value matches its respective placeholder name

The root-level valid property is true only when the group itself is valid and all descendant rules and groups are valid. Some diagnostics (e.g. INVALID_OPERATOR) don't affect valid, so before executing a query, reject any diagnostics you consider actionable and enforce a server-side whitelist of fields and operators.

Flat diagnostics array​

result.diagnostics is a flat array of DiagnosticEntry objects, each with an id, path, code, message, and source. Diagnostic codes include:

CodeSourceDescription
PLACEHOLDER_FIELDplaceholderRule has a placeholder field name
PLACEHOLDER_OPERATORplaceholderRule has a placeholder operator name
PLACEHOLDER_VALUEplaceholderRule has a placeholder value
MUTEDmutedRule or group is muted
CUSTOM_VALIDATORquery-validator / field-validatorFailed a custom validator
UNDEFINED_FIELDfield-checkRule references a field not in the fields config
UNREFERENCED_FIELDfield-checkA field in the config is not used by any rule
VALUE_TYPE_MISMATCHtype-checkValue is incompatible with the field's inputType, or isn't a boolean for a checkbox/switch valueEditorType
INVALID_OPERATORfield-checkOperator isn't in the field's operators (only checked when the field defines operators)
INVALID_VALUE_ARITYtype-checkbetween/notBetween value doesn't have exactly 2 items, or in/notIn value is empty
INVALID_VALUEtype-checkmatches/doesNotMatch value isn't a valid regular expression (rule is invalid and skipped by all formats)
VALUE_NOT_IN_OPTIONStype-checkValue isn't in the field's values (unless valueEditorType is text/textarea)
INVALID_STRUCTUREstructure-checkAn independent combinator group's rules don't alternate rule/group and combinator, or a standard group's rules contain a combinator string

All diagnostics except INVALID_STRUCTURE and the placeholder, muted, and validator checks are only produced when a fields config is provided. The INVALID_OPERATOR, INVALID_VALUE_ARITY, VALUE_NOT_IN_OPTIONS, VALUE_TYPE_MISMATCH, and INVALID_STRUCTURE diagnostics don't affect valid or stats. Operator, arity, values, and boolean checks are skipped for muted rules. Arity, values, and boolean checks are also skipped for unary operators (null/notNull, or arity: 'unary' in field.operators) and placeholder values, and values/boolean checks are skipped for valueSource: 'field'.

tip

diagnostics covers what a JSON Schema from getQueryJsonSchema can't express, so use it to check LLM-generated queries.

Target format check​

Set diagnosticsTargetFormat to a format with a formatCapabilities entry (currently "pinecone", "chroma", "qdrant", "weaviate", and "milvus") to report rules that format would skip. These entries have source: "format-check" and severity: "warning", and never affect valid or stats.

CodeDescription
UNSUPPORTED_OPERATOR_FOR_FORMATOperator can't be expressed in the target
UNSUPPORTED_VALUE_SOURCE_FOR_FORMATvalueSource: "field" unsupported by target
UNSUPPORTED_MATCH_MODE_FOR_FORMATMatch mode unsupported by target

For targets without native group negation, rules under an odd number of not groups are checked after De Morgan push-down (e.g. beginsWith → doesNotBeginWith), and the message notes the original operator.

Stats and field summary​

result.stats provides aggregate counts: totalRules, totalGroups, validRules, invalidRules, validGroups, invalidGroups.

result.fieldSummary is a record keyed by field name, where each value has ruleCount (total rules for that field) and invalidCount (invalid rules for that field).

Regular expressions​

The opt-in matches/doesNotMatch operators (see Regular expression operators) use unanchored "find" semantics in every format: a rule matches if the pattern is found anywhere in the field value. Formats whose native regex operator requires a full-string match wrap the pattern as .*(?:pattern).*. Flags come from the rule's regexFlags property; flags a format can't express are dropped, and g/y are always ignored. Rules with invalid patterns (per JavaScript's RegExp) are skipped.

Formatmatches outputFlags
jsonata$contains(f, /pattern/flags)i, m
mongodb_query/mongodb{ f: { $regex: 'pattern', $options: 'flags' } }native
spelf matches '(?i).*(?:pattern).*'inline
celf.matches("(?i)pattern") (RE2)inline
elasticsearchregexp query (Lucene syntax), wrapped in .*(…).*case_insensitive for i
sql/parameterized*Per preset: PostgreSQL ~/~*, MySQL/Oracle REGEXP_LIKE, SQLite REGEXPper dialect
sequelizeOp.regexp/Op.iRegexpi
jsonlogiccustom regex operation (register jsonLogicRegexOperator)third argument
cypher/gqlf =~ '(?i).*(?:pattern).*'inline
gremlinregex('(?i)pattern') (TextP.regex)inline
sparqlREGEX(?f, "pattern", "flags")native
natural_languagef matches regular expression 'pattern' with flags 'i'described
others (LDAP, Prisma, TanStack DB, vector DBs, ANSI/MSSQL SQL)falls back to contains/doesNotContain with the pattern as a literaln/a
caution

Patterns are passed through as-is. Regex dialects differ between targets (JavaScript, PCRE, POSIX, Java, RE2, Lucene, XPath), so a pattern that works in one may fail or behave differently in another. Notes:

  • SQLite has no built-in REGEXP function; register one (e.g. the sqlean regexp extension) before running the output. Flags are inlined as (?i).
  • MySQL's plain REGEXP (used when there are no flags) follows the column collation, so it's case-insensitive on _ci collations.
  • PostgreSQL only gets the i flag (~*/!~*); m/s are dropped.
  • ElasticSearch uses Lucene regex syntax, which has no \d-style classes or inline flags.

See the exact output for each format in the tests and the defaultRuleProcessor* source; some details (e.g. negation style) vary by format.

Configuration​

Pass an object as the second argument for fine-grained output control:

Parse numbers​

Render values as numbers instead of quoted strings using parseNumbers: true. See Number parsing for details.

Preserve value order​

formatQuery sorts "between"/"notBetween" values in ascending order when parseNumbers renders them as numbers. Disable with preserveValueOrder:

const query = {
rules: [{ field: 'age', operator: 'between', value: [30, 20] }],
};

formatQuery(query, { format: 'sql', parseNumbers: true });
/*
"(age between 20 and 30)"
*/

formatQuery(query, { format: 'sql', parseNumbers: true, preserveValueOrder: true });
/*
"(age between 30 and 20)"
*/
caution

This can create conditions that always evaluate to false. SQL's X BETWEEN Y AND Z equals X >= Y AND X <= Z—if Y > Z, no X value satisfies both conditions.

formatQuery assumes users mean "X is between points Y and Z" regardless of direction.

Rule processor​

Customize individual rule output using ruleProcessor. Only validated rules reach this function:

ruleProcessor(rule, { escapeQuotes, fieldData, ...otherOptions });

Arguments: RuleType object and ValueProcessorOptions object with escapeQuotes (true for string values, false for field names), fieldData (corresponding Field object), and other formatQuery options.

The default rule processors for each format are available as exports from react-querybuilder:

  • defaultRuleProcessorCEL
  • defaultRuleProcessorElasticSearch
  • defaultRuleProcessorJSONata
  • defaultRuleProcessorJsonLogic
  • defaultRuleProcessorMongoDB
  • defaultRuleProcessorMongoDBQuery
  • defaultRuleProcessorNL
  • defaultRuleProcessorSpEL
  • defaultRuleProcessorSQL
  • defaultRuleProcessorParameterized
  • defaultRuleProcessorTanStackDB

Refer to the source code to determine the appropriate return type for custom rule processors.

Use the appropriate default rule processor as a fallback so your custom processor doesn't cover all cases:

const query: RuleGroupType = {
combinator: 'and',
not: false,
rules: [
{ field: 'firstName', operator: 'has', value: 'S' },
// non-standard operator ^^^^^
{ field: 'lastName', operator: '=', value: 'Vai' },
],
};

const customRuleProcessor: RuleProcessor = (rule, options) => {
// The "has" operator is not handled by the default processor
if (rule.operator === 'has') {
return { in: [rule.value, { var: rule.field }] };
}

// Defer to the default processor for all other operators
return defaultRuleProcessorJsonLogic(rule, options);
};

formatQuery(query, { format: 'jsonlogic', ruleProcessor: customRuleProcessor });
/*
{
and: [
{ in: ["S", { var: "firstName" }] },
{ "==": [{ var: "lastName" }, "Vai"] }
]
}
*/

This SQL example (using Oracle syntax) demonstrates the generation of a case-insensitive condition:

// `query` is the same as in the previous example

const customRuleProcessor: RuleProcessor = (rule, options) => {
if (rule.operator === 'has') {
return `UPPER(${rule.field}) LIKE UPPER('%${rule.value}%')`;
}

return defaultRuleProcessorSQL(rule, options);
};

formatQuery(query, { format: 'sql', ruleProcessor: customRuleProcessor });
/*
"(UPPER(firstName) LIKE UPPER('%S%') and lastName = 'Vai')"
^------------custom--------------^ ^------default-----^
*/

Generating parameter names​

The "parameterized" and "parameterized_named" formats require rule processors to return an object resembling formatQuery's return type for these formats. The getNextNamedParam utility helps generate unique parameter names. The example below matches the Oracle SQL example above, but uses "parameterized_named" format.

const customRuleProcessor: RuleProcessor = (rule, options) => {
if (rule.operator === 'has') {
// TIP: `getNextNamedParam` can be called multiple times in case your SQL
// requires multiple unique parameters (e.g., in a "between" condition).
// Each call will generate a new name.
const paramName = options.getNextNamedParam!(rule.field);
return {
sql: `UPPER(${rule.field}) LIKE UPPER('%' || ${options.paramPrefix}${paramName} || '%')`,
params: { [paramName]: rule.value },
};
}

return defaultRuleProcessorSQLParameterized(rule, options);
};

formatQuery(query, { format: 'parameterized_named', ruleProcessor: customRuleProcessor });
/*
{
sql: "(UPPER(firstName) LIKE UPPER('%' || :firstName_1 || '%') and lastName = :lastName_1)",
params: {
firstName_1: "S",
lastName_1: "Vai"
}
}
*/

Value processor​

valueProcessor accepts the same arguments as ruleProcessor, but only affects the "value" portion (to the right of the operator) for "sql" format. If both are provided, ruleProcessor takes precedence.

tip

For all formats except "sql", valueProcessor is a synonym for ruleProcessor. Use ruleProcessor unless exporting SQL and only customizing the value portion.

// `query` is the same as in the previous example

const customValueProcessor: ValueProcessorByRule = (rule, options) => {
if (rule.operator === 'has') {
return `'%${rule.value}%'`;
}

return defaultValueProcessorByRule(rule, options);
};

formatQuery(query, { format: 'sql', valueProcessor: customValueProcessor });
/*
"(firstName like '%S%' and lastName = 'Vai')"
^---default---^ ^---^-custom ^--default--^
*/

Legacy valueProcessor behavior​

caution

The legacy valueProcessor signature exists for backwards compatibility, but avoid it. Options aren't passed in, making it difficult to correctly fall back to default processors.

If the valueProcessor function accepts three or more arguments (excluding those with default values), it's called like this:

valueProcessor(field, operator, value, valueSource);

No options or additional rule properties are passed as arguments. This prevents formatQuery from setting the escapeQuotes option, among other problems.

This legacy behavior is documented for completeness but not recommended.

const query: RuleGroupType = {
combinator: 'and',
not: false,
rules: [
{ field: 'instrument', operator: 'in', value: ['Guitar', 'Vocals'] },
{ field: 'lastName', operator: '=', value: 'Vai' },
],
};

const customValueProcessor = (field, operator, value) => {
if (operator === 'in') {
// Assuming `value` is an array, such as from a multi-select
return `(${value.map(v => `'${v.trim()}'`).join(',')})`;
}

return defaultValueProcessor(field, operator, value);
};

formatQuery(query, { format: 'sql', valueProcessor: customValueProcessor });
/*
"(instrument in ('Guitar','Vocals') and lastName = 'Vai')"
*/

Default value processors using the legacy signature are available for some query language formats.

FormatCurrent signature (recommended)Legacy signature (not recommended)
"sql"defaultValueProcessorByRuledefaultValueProcessor
"parameterized"defaultValueProcessorByRuledefaultValueProcessor
"parameterized_named"defaultValueProcessorByRuledefaultValueProcessor
"cel"defaultValueProcessorCELByRuledefaultCELValueProcessor
"mongodb"defaultValueProcessorMongoDBByRuledefaultMongoDBValueProcessor
"spel"defaultValueProcessorSpELByRuledefaultSpELValueProcessor

Operator processor​

operatorProcessor accepts the same arguments as ruleProcessor, but only affects the "operator" portion for "sql", "parameterized", "parameterized_named", and "natural_language" formats.

formatQuery(query, {
format: 'sql',
// Convert all operators to uppercase
operatorProcessor: (rule, options) => defaultOperatorProcessorSQL(rule, options).toUpperCase(),
});
/*
"(firstName LIKE 'Stev%' and lastName IN ('Vai', 'Vaughan'))"
*/

Quote field names​

Some database engines wrap field names in backticks (`) or square brackets ([]). Configure this with the quoteFieldNamesWith option (string or array of two strings).

formatQuery(query, { format: 'sql', quoteFieldNamesWith: '`' });
/*
"(`firstName` = 'Steve' and `lastName` = 'Vai')"
*/

formatQuery(query, { format: 'sql', quoteFieldNamesWith: ['[', ']'] });
/*
"([firstName] = 'Steve' and [lastName] = 'Vai')"
*/

The closing quote character is escaped within field names by doubling it (a"b → "a""b", a]b → [a]]b]). This applies to the SQL-based formats ("sql", "parameterized", "parameterized_named"), but not "jsonata" or "natural_language".

:::caution Untrusted queries

Unquoted field names (the default) can't be escaped, and operators and combinators are always emitted verbatim. When formatting queries from untrusted sources, quote field names and validate fields, operators, and combinators against a whitelist on the server before calling formatQuery.

Other formats escape field names where the target language has an escape mechanism:

  • "cypher"/"gql": .-delimited segments that aren't plain identifiers are backtick-quoted (n.first name → n.`first name` ).
  • "gremlin": property keys are escaped within the '…' string literal.
  • "elasticsearch": field names inside Painless scripts (doc['…']) are escaped.
  • "mongodb_query"/"mongodb": field names inside $where JavaScript use bracket notation when they aren't plain identifiers (this["first name"]). Object keys and $field references are emitted as-is (a leading $ or embedded . keeps its MongoDB meaning).

"cel", "spel", "sparql", "ldap", "jsonata", and "milvus" have no identifier escape syntax, so field names are emitted verbatim. Validate them against a whitelist. The "pinecone", "chroma", "qdrant", and "weaviate" formats emit field names as plain object keys/values, which can't break out of the output structure, but should still be whitelisted.

:::

Field identifier chains​

To quote members of field identifier chains independently, use fieldIdentifierSeparator. A common value is ".".

In this example, assume the field names are musicians.firstName and musicians.lastName.

formatQuery(query, {
format: 'sql',
quoteFieldNamesWith: ['[', ']'],
fieldIdentifierSeparator: '.',
});
/*
"([musicians].[firstName] = 'Steve' and [musicians].[lastName] = 'Vai')"
*/

Quote values​

Some database engines can accept string literals in double quotes ("). This can be configured with the quoteValuesWith option which should be assigned a one-character string.

formatQuery(query, { format: 'sql', quoteValuesWith: '"' });
/*
"(firstName = "Steve" and lastName = "Vai")"
*/

Parameter prefix​

If the "parameterized_named" format is used, configure the parameter prefix used in the sql string with the paramPrefix option, should the default ":" be inappropriate.

const p = formatQuery(query, {
format: 'parameterized_named',
paramPrefix: '$',
});
/*
p.sql === "(firstName = $firstName_1 and lastName = $lastName_1)"
// ^^^ ^^^
*/

Retain parameter prefixes​

paramsKeepPrefix simplifies compatibility with SQLite. With "parameterized_named" format, params object keys maintain the paramPrefix string as it appears in the sql string (e.g. { ":param_1": "val" } instead of { "param_1": "val" }).

Numbered parameters​

For "parameterized" format, parameter placeholders in generated SQL are "?" by default. When numberedParams is true, placeholders become numbered indices starting with 1, incrementing left to right. Each placeholder number is prefixed with the configured paramPrefix string (default ":").

const p = formatQuery(query, {
format: 'parameterized',
paramPrefix: '$',
numberedParams: true,
});
/*
p.sql === "(firstName = $1 and lastName = $2)"
*/

Previously, manual post-processing was necessary for this effect.

Named parameters (value source)​

The getParameters option supports rules whose valueSource is "parameter". Provide the same function passed to the getParameters prop (names without a prefix):

formatQuery(query, {
format: 'sql',
getParameters: () => [{ name: 'p1', label: 'Param 1' }],
});

Behavior by format:

  • sql — the prefixed name is emitted inline (e.g. f1 = :p1).
  • parameterized — the name is emitted inline; positional placeholders are not pushed to params.
  • parameterized_named — the name is registered as a params key with a null placeholder value (respecting paramsKeepPrefix), to be supplied at execution time. (null rather than undefined, so the key is preserved by JSON.stringify.)
  • cel, spel, jsonlogic — the name is treated as an identifier/variable reference.
  • Other formats emit the name as a literal.

When getParameters is supplied, rules referencing a name not in the list are treated as invalid (dropped or handled per your validation options).

tip

The external parameter manager example demonstrates merging user-supplied values over the null placeholders produced by the "parameterized_named" format.

Concatenation operator​

Most SQL database dialects use the || operator to concatenate strings. SQL Server uses +, and MySQL uses the CONCAT function instead.

Configure the concatenation operator (used for "contains", "beginswith", and "endswith" operators when valueSource is "field") with the concatOperator option. formatQuery uses the ANSI standard || by default.

If the value is "CONCAT" (case-insensitive), the CONCAT function is used. (Note: Oracle SQL doesn't support more than two values in CONCAT, so avoid this option with Oracle. The default || operator is Oracle-compatible.)

const query = {
combinator: 'and',
rules: [
{ field: 'firstName', operator: '=', value: 'Kris' },
{ field: 'lastName', operator: 'beginswith', value: 'firstName', valueSource: 'field' },
],
};

formatQuery(query, { format: 'sql', concatOperator: '+' });
/*
"(firstName = 'Kris' and lastName like firstName + '%')"
*/

formatQuery(query, { format: 'sql', concatOperator: 'CONCAT' });
/*
"(firstName = 'Kris' and lastName like CONCAT(firstName, '%'))"
*/

Presets​

The preset option configures options known to enable or improve compatibility with particular query language dialects. Individual options override their respective preset values. Available presets:

info

If preset is from sqlDialectPresets, it only applies if format is undefined or one of the SQL-based formats.

DialectPreset options

'ansi'

{}

'sqlite'

{ "paramsKeepPrefix": true }

'oracle'

{ "numberedParams": true, "paramPrefix": ":" }

'mssql'

{
"quoteFieldNamesWith": ["[", "]"],
"concatOperator": "+",
"fieldIdentifierSeparator": ".",
"numberedParams": true,
"paramPrefix": "@"
}

Boolean values render as 1/0 (T-SQL has no TRUE/FALSE literals).

'mysql'

{
"concatOperator": "CONCAT",
"quoteFieldNamesWith": "`",
"fieldIdentifierSeparator": "."
}

Backslashes in string values are doubled when escapeQuotes is on (MySQL/MariaDB treat \ as an escape character in string literals).

'postgresql'

{ "quoteFieldNamesWith": "\"", "numberedParams": true, "paramPrefix": "$" }

Examples:

formatQuery(query, { format: 'parameterized', preset: 'postgresql' });
/*
{
sql: `("firstName" like $1 and "lastName" in ($2, $3))`,
params: ['Stev%', 'Vai', 'Vaughan']
}
*/

formatQuery(query, { format: 'sql', preset: 'mssql' });
/*
"([musicians].[firstName] = 'Kris' and [musicians].[lastName] like [musicians].[firstName] + '%')"
*/

Fallback expression​

fallbackExpression is a string included in output when formatQuery can't determine what to do for a particular rule or group. The intent is to maintain valid syntax while not affecting query criteria. If not provided, the default fallback expression for the format is used:

FormatDefault fallbackExpression
'sql''(1 = 1)'
'parameterized''(1 = 1)'
'parameterized_named''(1 = 1)'
'cypher' / 'gql''(1 = 1)'
'sparql''1 = 1'
'gremlin'''
'ldap'''
'mongodb''{"$and":[{"$expr":true}]}'
'mongodb_query'{"$and":[{"$expr":true}]}
'natural_language''1 is 1'
'cel''1 == 1'
'spel''1 == 1'
'jsonata''(1 = 1)'
'jsonlogic'false
'elasticsearch'{}
'pinecone'{}
'chroma'{}
'qdrant'{}
'weaviate'{}
'milvus'''
'drizzle'undefined
'prisma'{}
'sequelize'{}
'tanstack_db'eq(1, 1)

Value sources​

When a rule's valueSource property is "field", no parameters are generated.

const pf = formatQuery(
{
combinator: 'and',
rules: [
{ field: 'firstName', operator: '=', value: 'lastName', valueSource: 'field' },
{ field: 'firstName', operator: 'beginsWith', value: 'middleName', valueSource: 'field' },
],
},
'parameterized_named'
);

Output (JSON object):

{
"sql": "(firstName = lastName and firstName like middleName || '%')",
"params": {}
}

Placeholder values​

Rules where field, operator, or value matches the placeholder value (default "~") are excluded from output for most export formats (see Automatic validation). To use a different placeholder string, set the placeholderFieldName, placeholderOperatorName, or placeholderValueName options. These correspond to fields.placeholderName, operators.placeholderName, and values.placeholderName properties on the main component's translations prop object. This behavior for the value property only applies if placeholderValueName is explicitly set. The others use their defaults if undefined.

Internationalization​

These i18n options are specific to "natural_language" format.

Word order​

Based on constituent word order, the wordOrder option accepts all permutations of "SVO" ("SOV", "VSO", etc.) and outputs field, operator, and value in corresponding order (S = field, V = operator, O = value).

formatQuery(query, {
format: 'natural_language',
wordOrder: 'SOV',
});
// `First Name 'Steve' is`

Translations​

Map "and", "or", "true", and "false" to their translated equivalents, plus prefix and suffix options for rule groups.

The base prefix/suffix options are "groupPrefix" and "groupSuffix". The applicability of a group-related translation is determined by two conditions: (1) whether the group's not property is true, and (2) whether the combinator for the group is "xor". The base "group*" translations are the fallbacks for when neither condition is true. When one or more conditions are true, formatQuery will look for a property on the translations object that matches the base property with a suffix of underscore ("_") plus the condition ID ("not" or "xor").

For example, when a group has a not: true property, but the combinator is something other than "xor", formatQuery will look for the groupSuffix_not key.

formatQuery(query, {
format: 'natural_language',
translations: {
groupSuffix: 'is def the truth',
groupSuffix_not: 'is so not true',
},
});
// Given the following query:
// const query = {
// rules: [
// { rules: [{ field: 'firstName', operator: '=', value: 'Steve' }] },
// 'and',
// { not: true, rules: [{ field: 'firstName', operator: '=', value: 'Vai' }] },
// ]
// };
// ...potential output could be:
// `(First Name is 'Steve') is def the truth, and (Last Name is 'Vai') is so not true`

When not is falsy but the combinator is "xor", groupSuffix_xor will be used if it exists. Otherwise it will fall back to the default. If both conditions are true, the order of the suffixes doesn't matter: both "groupSuffix_not_xor" and "groupSuffix_xor_not" would be valid (although there is no guarantee which one will be used if both are present).

Rule separator​

By default, rules within a group are separated by a comma followed by a space (, ). Use the ruleSeparator translation key to change this. The value should include any trailing space if desired — for example, '; ' for a semicolon separator, or '、' (ideographic comma, no space) for Japanese.

formatQuery(query, {
format: 'natural_language',
translations: { ruleSeparator: '、', and: 'かつ' },
});
// `First Name is 'Steve'、かつ Last Name is 'Vai'`
Between conjunction​

The conjunction between the two values in a "between" expression defaults to the and translation (or "and" if not set). Use the betweenAnd translation key to override it independently. This is useful for languages where the logical conjunction (used between rules) and the range conjunction (used in "between X and Y") are different words.

formatQuery(query, {
format: 'natural_language',
translations: { and: 'かつ', betweenAnd: 'と' },
});
// `Age is between '12' と '14'`
// Rules are still joined with 'かつ'
Constituent particles​

By default, the word order constituents (Subject, Verb, Object) are separated by a single space. Use afterSubject, afterVerb, and afterObject to insert particles or custom separators after each constituent. Each defaults to ' ' (space) when not set.

This is essential for languages that use grammatical particles between constituents — for example, Japanese requires が (subject marker) after the subject:

formatQuery(query, {
format: 'natural_language',
wordOrder: 'SOV',
translations: { afterSubject: 'が' },
operatorMap: { '=': 'である' },
});
// `First Nameが'Steve' である`
List separator​

The values in an "in" or "notin" list are separated by ', ' (comma + space) by default, with an Oxford comma before the final item when there are three or more values. Set the listSeparator translation key to change the separator. When a custom listSeparator is set, the Oxford comma is automatically disabled.

formatQuery(query, {
format: 'natural_language',
translations: { listSeparator: '、', or: 'または' },
});
// `Color is one of the values ('Red'、'Green' または 'Blue')`

Operator map​

operatorMap is a map of operators to their natural language equivalents. If the result can differ based on the valueSource, the key should map to an array where the second element represents the string to be used when valueSource is "field"; the first element will be used in all other cases.

formatQuery(query, {
format: 'natural_language',
operatorMap: {
'=': 'is most assuredly',
'!=': ['is not', 'differs from'],
},
});
// `First Name is most assuredly 'Steve', and Last Name differs from First Name`

Sample language configurations​

These examples show how wordOrder, operatorMap, and translations combine to produce natural-sounding output in different languages. Each example targets a different constituent word order family.

Japanese (SOV with particles)​

Japanese uses Subject-Object-Verb order and requires the particle が after the subject. Ideographic punctuation (、) replaces commas, and the から…の間 pattern is more natural than と…の間 for ranges.

formatQuery(query, {
format: 'natural_language',
wordOrder: 'SOV',
operatorMap: {
'=': ['である', 'と同じ値である'],
'>': ['より大きい', 'の値より大きい'],
beginswith: ['で始まる', 'の値で始まる'],
in: ['のいずれかである', 'と同じ値のいずれかである'],
between: ['の間である', 'の値の間である'],
// ... other operators
},
translations: {
and: 'かつ',
or: 'または',
true: '真',
false: '偽',
ruleSeparator: '、',
betweenAnd: 'から',
afterSubject: 'が',
afterObject: '',
listSeparator: '、',
groupSuffix: '',
},
});
// First Nameが'Stev'で始まる、かつ Ageが'28'より大きい
Spanish (SVO with translations)​

Spanish uses the same SVO order as English, so only operatorMap and translations are needed — no wordOrder change.

formatQuery(query, {
format: 'natural_language',
operatorMap: {
'=': ['es', 'es igual al valor de'],
'>': ['es mayor que', 'es mayor que el valor de'],
beginswith: ['comienza con', 'comienza con el valor de'],
in: ['es uno de los valores', 'es igual a un valor en'],
between: ['está entre', 'está entre los valores de'],
// ... other operators
},
translations: {
and: 'y',
or: 'o',
true: 'verdadero',
false: 'falso',
groupSuffix: 'es verdadero',
groupSuffix_not: 'no es verdadero',
},
});
// First Name comienza con 'Stev', y Age es mayor que '28'
Welsh (VSO)​

Welsh uses Verb-Subject-Object order, placing the operator first.

formatQuery(query, {
format: 'natural_language',
wordOrder: 'VSO',
operatorMap: {
'=': ['yw', "yr un fath â'r gwerth yn"],
'>': ['yn fwy na', "yn fwy na'r gwerth yn"],
beginswith: ['yn dechrau gyda', "yn dechrau gyda'r gwerth yn"],
in: ["yn un o'r gwerthoedd", "yr un fath ag un o'r gwerthoedd yn"],
between: ['rhwng', 'rhwng y gwerthoedd yn'],
// ... other operators
},
translations: {
and: 'a',
or: 'neu',
true: 'gwir',
false: 'gau',
betweenAnd: 'a',
groupSuffix: 'yn wir',
groupSuffix_not: 'ddim yn wir',
},
});
// yn dechrau gyda First Name 'Stev', a yn fwy na Age '28'
tip

Complete configurations for these languages (plus Korean) are available in the demo — use the language selector on the "Natural language" export tab to preview the output interactively.

Rule group processor​

formatQuery processes, validates, and augments configuration options before passing the query and "final" options object to the appropriate rule group processor for the requested format.

To leverage this pre-processing but generate custom output, use the ruleGroupProcessor option. The function is called with the rule group and "final" prepared options object:

ruleGroupProcessor(ruleGroup, finalOptions);

Note: The ruleGroupProcessor option overrides the format option.

The default rule group processors for each format are available as exports from react-querybuilder:

  • defaultRuleGroupProcessorCEL
  • defaultRuleGroupProcessorElasticSearch
  • defaultRuleGroupProcessorJSONata
  • defaultRuleGroupProcessorJsonLogic
  • defaultRuleGroupProcessorMongoDB
  • defaultRuleGroupProcessorMongoDBQuery
  • defaultRuleGroupProcessorNL
  • defaultRuleGroupProcessorSpEL
  • defaultRuleGroupProcessorSQL
  • defaultRuleGroupProcessorParameterized
  • defaultRuleGroupProcessorTanStackDB

Use the appropriate default rule group processor as a fallback so your custom processor doesn't need to cover all cases:

const query: RuleGroupType = {
combinator: 'and',
not: false,
rules: [
{ combinator: 'and', rules: [] },
// empty rules array ^^^^^^^^^
{ field: 'firstName', operator: 'beginsWith', value: 'S' },
],
};

const customRuleGroupProcessor: RuleGroupProcessor<string> = (ruleGroup, options) => {
if (ruleGroup.rules.length === 0) {
// Normally, empty rule groups are ignored, but here they evaluate to false
return '(1 = 0)';
}

// Defer to the default rule group processor for all other operators
return defaultRuleGroupProcessorSQL(ruleGroup, options);
};

formatQuery(query, { ruleGroupProcessor: customRuleGroupProcessor });
/*
"((1 = 0) and firstName LIKE 'S%')"
*/

Validation​

Validation options (validator and fields – see Validation) only affect output when format is not "json" or "json_without_ids". If the validator function returns false, the fallbackExpression is returned. Otherwise, groups and rules marked as invalid (by the validation map from the validator function or field-based validator function) are ignored.

Example:

const query: RuleGroupType = {
id: 'root',
rules: [
{ id: 'r1', field: 'firstName', value: '', operator: '=' },
{ id: 'r2', field: 'lastName', value: 'Vai', operator: '=' },
],
combinator: 'and',
not: false,
};

// Example 1
// Query is invalid based on the validator function
formatQuery(query, {
format: 'sql',
validator: () => false,
});
/*
"(1 = 1)" <-- see `fallbackExpression` option
*/

// Example 2
// Rule "r1" is invalid based on the validation map
formatQuery(query, {
format: 'sql',
validator: () => ({ r1: false }),
});
/*
"(lastName = 'Vai')" <-- skipped `firstName` rule with `id === 'r1'`
*/

// Example 3
// Rule "r1" is invalid based on the field validator for `firstName`
formatQuery(query, {
format: 'sql',
fields: [{ name: 'firstName', validator: () => false }],
});
/*
"(lastName = 'Vai')" <-- skipped `firstName` rule because field validator returned `false`
*/

Muted rules and groups​

Rules and groups with the muted property set to true are excluded from output for all formats except "json" and "json_without_ids", similar to invalid rules and groups. This allows temporary exclusion of conditions without removing them from the query structure.

const query: RuleGroupType = {
combinator: 'and',
rules: [
{ field: 'firstName', operator: '=', value: 'Steve' },
{ field: 'lastName', operator: '=', value: 'Vai', muted: true },
],
};

formatQuery(query, 'sql');
// "(firstName = 'Steve')" - lastName rule is excluded

When a group is muted, it's replaced with the fallback expression:

const query: RuleGroupType = {
combinator: 'and',
rules: [
{ field: 'firstName', operator: '=', value: 'Steve' },
{
combinator: 'or',
rules: [
{ field: 'lastName', operator: '=', value: 'Vai' },
{ field: 'instrument', operator: '=', value: 'Guitar' },
],
muted: true,
},
],
};

formatQuery(query, 'sql');
// "(firstName = 'Steve' and (1 = 1))" - muted group becomes fallback
tip

Enable mute functionality in the UI by setting showMuteButtons to true on the main QueryBuilder component.

Automatic validation​

To minimize invalid syntax, formatQuery performs basic validation for "in", "notIn", "between", and "notBetween" operators for all formats except "json" and "json_without_ids", even without specified validator functions or field validators.

  • Rules with "in" or "notIn" operators are invalid if the value is neither an array with at least one element (value.length > 0) nor a non-empty string.
  • Rules with "between" or "notBetween" operators are invalid if the value is neither an array with at least two elements (value.length >= 2) nor a string with at least one comma not at the first or last position (value.split(',').length >= 2, and neither element is empty).
  • Rules where field, operator, or value match their respective placeholder are invalid:
    field === placeholderFieldName ||
    operator === placeholderOperatorName ||
    (placeholderValueName !== undefined && value === placeholderValueName)