findByJson / findWithJsonExpr Dynamic Query Guide
Evidence boundary: these Java API and parser rules are based on generated request/runtime source review. The current Java Golden Path remains blocked before runtime execution, so provider behavior, tenant isolation, resource limits, and chain-field cases require application integration tests. Start with the bounded Dynamic JSON Query recipe before exposing this mechanism to untrusted input.
Overview
TeaQL supports dynamic query construction from a JSON expression.
In generated request code this is commonly exposed as:
Q.orders().findWithJsonExpr(params)
In the core runtime, the JSON string is parsed and merged into the request through:
BaseRequest.internalFindWithJsonExpr(String jsonNodeExpr)
The dynamic JSON expression can add:
- Field filters
- Chain-field filters such as
customer.name - Ordering
- Offset/limit
- Page/page-size
This is useful when a frontend or generic admin API needs flexible search without writing one Java method for every possible filter combination.
Runtime Flow
The runtime flow is:
JSON string
-> DynamicSearchHelper.jsonFromString(...)
-> DynamicSearchHelper.mergeClauses(...)
-> addJsonFilter(...)
-> addJsonOrderBy(...)
-> addJsonLimiter(...)
-> addJsonPager(...)
-> request.comment("Query orders").purpose("Load data").executeForList(userContext)
In BaseService, dynamic search actions also use this mechanism:
if (ObjectUtil.isNotEmpty(parameter)) {
baseRequest.internalFindWithJsonExpr(parameter);
}
That means generated service endpoints can accept JSON search parameters and merge them into the generated request.
Basic Usage
Java
public WebResponse searchOrder(UserContext context, String params) {
SmartList<Order> orders = Q.orders().selectAll()
.findWithJsonExpr(params)
.orderByIdDescending()
.comment("Query orders").purpose("Load data")
.executeForList(context);
return WebResponse.of(orders);
}
This example assumes trusted tenant, actor, permissions, and request policy
were installed while UserContext was initialized. Runtime policy—not an
optional builder call in this method—must apply the tenant boundary before
provider execution.
JSON
{
"status": "SHIPPED",
"code": "ORD-2026",
"_orderBy": {
"field": "id",
"useAsc": false
},
"_page": 1,
"_pageSize": 20
}
Resulting Intent
status = SHIPPED
code contains ORD-2026
order by id desc
page 1, page size 20
JSON Filter Rules
findWithJsonExpr only applies a JSON field as a filter when the field is known to the current request.
Example:
{
"code": "ORD",
"status": "NEW"
}
Java request:
Q.orders().findWithJsonExpr(params)
.comment("Query orders").purpose("Load data")
.executeForList(userContext);
The dynamic helper checks whether code and status are fields of Order. Unknown fields are ignored for self-field filtering.
Value Type to Operator Rules
TeaQL infers the operator from the JSON value type.
| JSON Value | Inferred Operator | Example |
|---|---|---|
| String | CONTAIN by default | "code": "ORD" |
| Number | EQUAL | "id": 1001 |
| Boolean | EQUAL | "finished": false |
| Array of strings | IN | "status": ["NEW", "SHIPPED"] |
Array of objects with id | IN | "customer": [{"id": 1}, {"id": 2}] |
| Array with 2 values | BETWEEN for range-style values | "createTime": [1714521600000, 1717200000000] |
"__is_null__" | IS_NULL | "shipTime": "__is_null__" |
"__is_not_null__" | IS_NOT_NULL | "shipTime": "__is_not_null__" |
Important detail:
- Normal string values are treated as
CONTAIN. - A single-value string array such as
"name": ["product name"]is treated asINwith one value. In practice, this behaves like exact equality for that value. __is_null__and__is_not_null__are special string values.- Date/time fields convert array values into
Dateobjects when the target field is a date/time field.
String Contains
JSON
{
"code": "ORD-2026"
}
Meaning
Q.orders().withCode(Operator.CONTAIN, "ORD-2026")
Use this for fuzzy search boxes.
Exact Numeric and Boolean Filters
JSON
{
"id": 1001,
"finished": false
}
Meaning
Q.orders()
.filterByFinished(false)
.filterById(1001L)
.comment("Query orders")
.purpose("Explain JSON exact matches")
Numbers and booleans are treated as exact matches.
IN Filters
JSON
{
"status": ["NEW", "SHIPPED", "DELIVERED"]
}
Meaning
Q.orders()
.withStatus(Operator.IN, List.of("NEW", "SHIPPED", "DELIVERED"))
.comment("Query orders")
.purpose("Explain JSON IN filters")
This is useful for multi-select filters.
For exact matching with one string value, pass a single-value array:
{
"name": ["product name"]
}
This is parsed as IN with one value:
Q.products().withName(Operator.IN, "product name").comment("Query products").purpose("Load data")
For normal data, this is equivalent to:
name = product name
Object Reference Filters
If the field is an object reference, JSON objects can provide id.
JSON
{
"customer": {
"id": 10001
}
}
Meaning
The JSON value is unwrapped to the id:
Q.orders()
.filterByCustomer(Customer.refer(10001L))
.comment("Query orders")
.purpose("Explain reference filters")
For multiple references:
{
"customer": [
{"id": 10001},
{"id": 10002}
]
}
This is treated as an IN filter.
Null Checks
Is Null
{
"shipTime": "__is_null__"
}
Is Not Null
{
"shipTime": "__is_not_null__"
}
These values map to TeaQL Operator.IS_NULL and Operator.IS_NOT_NULL.
Range and Date/Time Filters
For date/time fields, a two-value array can be used as a range.
JSON
{
"createTime": [1714521600000, 1717200000000]
}
Meaning
Q.orders().withCreateTime(Operator.BETWEEN, startDate, endDate).comment("Query orders").purpose("Load data")
The helper unwraps date/time values into Date objects when the target field is a date/time field.
Use millisecond timestamps when passing date/time values through JSON.
Ordering
Use _orderBy.
Single Text Field
{
"_orderBy": "id"
}
This means:
order by id desc
The single text form defaults to descending because the helper calls:
addOrderBy(baseRequest, fieldValue.asText(), false)
Object Form
{
"_orderBy": {
"field": "createTime",
"useAsc": false
}
}
Multiple Order Rules
{
"_orderBy": [
{
"field": "status",
"useAsc": true
},
{
"field": "createTime",
"useAsc": false
}
]
}
Ordering only applies when the field is one of the current request's self fields.
Offset and Size
Use _start and _size for low-level offset/limit control.
JSON
{
"_start": 40,
"_size": 20
}
Meaning
offset 40
limit 20
This is useful for admin grids or infinite scrolling.
Page and Page Size
Use _page and _pageSize for page-based pagination.
JSON
{
"_page": 3,
"_pageSize": 20
}
Meaning
page 3, page size 20
offset = (3 - 1) * 20
The runtime calculates offset using the page number and request size.
Chain Field Filters
Dynamic JSON supports chain fields such as:
{
"customer.name": "Alice"
}
This means the filter is applied to the child request for customer.
Important Rule
The parent request must already contain the child sub-request.
Correct:
SmartList<Order> orders = Q.orders()
.selectCustomer()
.findWithJsonExpr("""
{
"customer.name": "Alice"
}
""").comment("Query orders").purpose("Load data")
.executeForList(userContext);
Better when customizing selected customer fields:
SmartList<Order> orders = Q.orders().selectCustomer(Q.customers().selectName())
.findWithJsonExpr(params)
.comment("Query orders").purpose("Load data")
.executeForList(userContext);
If the request has no customer sub-request, a chain-field filter cannot be attached safely.
Chain Field on Nested Collections
Example model:
Order
-> orderLineList
-> product
JSON:
{
"orderLineList.product.name": "Nitrogen"
}
Java:
SmartList<Order> orders = Q.orders().selectOrderLineList(
Q.orderLines()
.selectProduct(Q.products().selectName()))
.findWithJsonExpr(params)
.comment("Query orders").purpose("Load data")
.executeForList(userContext);
The chain path must match the selected request path:
orderLineList -> product -> name
Combine Fixed Q Conditions with Dynamic JSON
The recommended pattern is:
Q.orders().selectAll()
.findWithJsonExpr(params)
.withVersionGreaterThan(0)
.orderByIdDescending()
.comment("Query orders").purpose("Load data")
.executeForList(userContext);
Use fixed Q conditions for business shape that the user must not control:
- Version > 0
- Soft-delete rules
Use trusted UserContext and mandatory runtime policy for tenant, merchant,
permission, and other security boundaries. A fixed Q condition may additionally
narrow the operation but cannot be the only enforcement.
Use JSON for user-driven filters:
- Keyword search
- Status selection
- Date range
- Sorting
- Pagination
Example: Search Employee
An application service can use this shape:
public WebResponse searchEmployee(UserContext context, String params) {
SmartList<Employee> employees = Q.employees().selectAll()
.selectEmployeePermissionList()
.selectEmployeeCertificationList()
.findWithJsonExpr(params)
.comment("Query employees").purpose("Load data")
.executeForList(context);
return WebResponse.of(employees);
}
What is fixed:
- Merchant scope
- Selected child lists
What is dynamic:
- Search filters from
params - Sorting
- Paging
Example: Search Polymorphic Reports
public WebResponse searchSubTaskReport(UserContext context, String params) {
SmartList<SubTaskReport> reports = Q.subTaskReports()
.selectAll()
.tryEnhanceChildren()
.enhanceChild(
Q.checkReports()
.selectCheckStandard()
.returnType(CheckReport.class)
.selectCheckReportItemList())
.enhanceChild(Q.fillReports().selectProduct())
.enhanceChild(
Q.analysisReports()
.selectProduct()
.selectStandard()
.selectAnalysisReportItemList(
Q.analysisReportItems()
.selectType()
.selectMeasureUnit()
.selectComponent()))
.findWithJsonExpr(params)
.orderByIdDescending()
.count()
.comment("Query polymorphic reports")
.purpose("Render the authorized report search results")
.executeForList(context);
return WebResponse.of(reports);
}
This combines:
- Polymorphic enhancement
- Nested child selection
- Dynamic JSON search
- Tenant/merchant protection
- Default ordering
- Count support
Example JSON for an Admin Grid
{
"name": "Alice",
"status": ["ACTIVE", "LOCKED"],
"merchant": {
"id": 10001
},
"_orderBy": [
{
"field": "status",
"useAsc": true
},
{
"field": "id",
"useAsc": false
}
],
"_page": 1,
"_pageSize": 20
}
Recommended Java:
Q.employees().selectAll()
.findWithJsonExpr(params)
.comment("Query employees").purpose("Load data")
.executeForList(userContext);
The endpoint must reject a merchant field in JSON. Independently, mandatory
runtime policy applies the trusted merchant scope from UserContext; tenant
safety does not depend on this query remembering a merchant filter.
Building JSON Dynamically on the Frontend
A frontend can build JSON from a search form:
const params = {
name: form.keyword || undefined,
status: form.statusList,
createTime: form.start && form.end ? [form.start.getTime(), form.end.getTime()] : undefined,
_orderBy: {
field: form.sortField || "id",
useAsc: form.sortDirection === "asc",
},
_page: page,
_pageSize: pageSize,
};
const cleaned = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== undefined && value !== "")
);
Send JSON.stringify(cleaned) as the params argument.
Backend Builder Pattern
When building dynamic JSON on the backend, keep it typed at the boundary:
public class OrderSearchParams {
private String keyword;
private List<String> status;
private Long startTime;
private Long endTime;
private Integer page;
private Integer pageSize;
// getters and setters
}
Convert it to JSON only at the findWithJsonExpr boundary:
Map<String, Object> json = new LinkedHashMap<>();
json.put("code", request.getKeyword());
json.put("status", request.getStatus());
json.put("createTime", List.of(request.getStartTime(), request.getEndTime()));
json.put("_page", request.getPage());
json.put("_pageSize", request.getPageSize());
String params = JSONUtil.toJsonStr(json);
return Q.orders().selectAll()
.findWithJsonExpr(params)
.comment("Query orders").purpose("Load data")
.executeForList(userContext);
Security Rules
Dynamic JSON should not become a permission bypass. Keep trusted tenant,
identity, permissions, and policy outside JSON and inside server-initialized
UserContext. Runtime policy must cover list, ID, relation, aggregate, and
mutation paths.
Java query code may add further business restrictions:
.filterByMerchant(userContext.getMerchant())
.withVersionGreaterThan(0)
Such filters only narrow the business operation. They do not replace the context-installed security policy and must not be required at every call site for tenant safety.
Do not let JSON decide:
- Tenant
- Merchant
- Current user scope
- Permission boundaries
- Soft-delete behavior
- System-only fields
Use JSON for search convenience, not for trust decisions.
Validation Rules
Before calling findWithJsonExpr, validate:
- JSON string is not too large
- Page size is within allowed limits
- Sort field is allowed for the current screen
- Chain fields are allowed for the current API
- Date ranges are reasonable
- User is allowed to search the target entity
Example:
public String normalizeOrderSearchParams(String params) {
JsonNode json = new ObjectMapper().readTree(params);
int pageSize = json.path("_pageSize").asInt(20);
if (pageSize > 200) {
throw new IllegalArgumentException("Page size is too large");
}
return params;
}
Parse with your project JSON library before passing the string to TeaQL.
Common Mistakes
1. Expecting Strings to Mean Exact Match
JSON:
{
"code": "ORD"
}
This means CONTAIN, not exact equality.
For exact business filters, prefer generated Q methods:
Q.orders().filterByCode("ORD-1001")
2. Using Chain Fields Without Selecting the Child Request
Problem:
Q.orders()
.findWithJsonExpr("{\"customer.name\":\"Alice\"}")
.comment("Query orders")
.purpose("Load orders for matching customer names")
.executeForList(context);
Safer:
Q.orders().selectCustomer(Q.customers().selectName())
.findWithJsonExpr("{\"customer.name\":\"Alice\"}")
.comment("Query orders").purpose("Load data")
.executeForList(context);
3. Letting JSON or a Call Site Control Tenant Scope
Problem: this is unsafe if tenant isolation depends on a builder method the service author may forget.
Q.orders().findWithJsonExpr(params)
.comment("Query orders").purpose("Load data")
.executeForList(context);
Safer: validate JSON, then rely on the mandatory policy installed during
trusted UserContext assembly.
Q.orders().findWithJsonExpr(params)
.comment("Query orders").purpose("Load data")
.executeForList(context);
The policy reads the authoritative tenant from context and combines its predicate
with the request at runtime. A missing tenant or policy must fail closed.
4. Unbounded Page Size
Always cap _size and _pageSize at the API boundary.
Recommended API Shape
Controller:
@PostMapping("/orders/search")
public WebResponse searchOrders(
@TQLContext CustomUserContext userContext,
@RequestBody String params) {
return orderService.searchOrders(userContext, params);
}
Service:
public WebResponse searchOrders(CustomUserContext userContext, String params) {
String normalized = OrderSearchPolicy.normalize(params, userContext);
SmartList<Order> orders = Q.orders().selectAll()
.selectCustomer(Q.customers().selectName())
.findWithJsonExpr(normalized)
.orderByIdDescending()
.comment("Query orders").purpose("Load data")
.executeForList(userContext);
return WebResponse.of(orders);
}
Policy:
public class OrderSearchPolicy {
public static String normalize(String params, CustomUserContext userContext) {
// Validate page size, allowed fields, allowed sort, and request size.
return params;
}
}
Summary
findWithJsonExpr is best used as a dynamic search layer on top of generated TeaQL requests.
Use it for:
- User-driven filters
- Admin search forms
- Dynamic sorting
- Pagination
- Chain-field filters when the child request is selected
Keep fixed business shape in generated/handwritten Java and security state in
trusted UserContext plus runtime policy:
- Tenant and merchant scope: runtime policy from
UserContext - Permission boundaries: runtime policy from
UserContext - Soft-delete and version rules
- Required graph shape
- Default ordering
This gives teams dynamic query flexibility without losing TeaQL's strongly typed domain model and UserContext-based runtime control.