Skip to main content

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 ValueInferred OperatorExample
StringCONTAIN by default"code": "ORD"
NumberEQUAL"id": 1001
BooleanEQUAL"finished": false
Array of stringsIN"status": ["NEW", "SHIPPED"]
Array of objects with idIN"customer": [{"id": 1}, {"id": 2}]
Array with 2 valuesBETWEEN 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 as IN with 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 Date objects 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.


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.