Skip to main content

Dynamic JSON Query

Evidence status: the Java parser and generated request shape have been reviewed, but the current generated Java baseline is blocked before execution. The allowlist, tenant-isolation, resource-limit, and provider behavior below still require integration tests before production adoption. No equivalent Rust JSON-query contract is claimed by this recipe.

Problem

Accept flexible filters, ordering, and pagination from a search form without creating one endpoint per filter combination—and without letting untrusted JSON control tenant scope, permissions, soft-delete behavior, or query cost.

When to Use It

Use findWithJsonExpr(...) for a bounded search screen whose permitted fields and operations are known by the application.

Do not use it:

  • as a public arbitrary-query language;
  • to choose tenant, merchant, user, or permission scope;
  • when a fixed generated query expresses the operation more clearly;
  • before defining request-size, field, sort, page-size, and chain-path limits.

Inputs and Trust Boundary

For an illustrative order-search screen, allow only a small contract:

{
"code": "ORD-2026",
"status": ["NEW", "SHIPPED"],
"createTime": [1714521600000, 1717200000000],
"_orderBy": {
"field": "id",
"useAsc": false
},
"_page": 1,
"_pageSize": 20
}

Keep these outside the JSON and derive them from trusted server context:

  • tenant or merchant identity;
  • current-user and permission constraints;
  • soft-delete/version constraints;
  • required relation shape;
  • application maximum page size and request size.

The runtime recognizes fields on the generated request and ignores unknown self-field filters. That behavior is not input validation: silently ignoring a misspelled or forbidden field can produce a broader result than the caller expected. Reject fields outside the endpoint allowlist before merging JSON.

Java Path

Validate and normalize the request at the handwritten API boundary, then merge it into a generated request that still applies trusted constraints:

public SmartList<Order> searchOrders(
CustomUserContext ctx,
String rawParams) {
String params = OrderSearchPolicy.validateAndNormalize(rawParams);

return Q.orders()
.selectAll()
.findWithJsonExpr(params)
.filterByMerchant(ctx.getMerchant())
.orderByIdDescending()
.comment("Query merchant orders")
.purpose("Render the bounded order search results")
.executeForList(ctx);
}

Q.orders(), selectors, filters, and ordering methods are model-generated. Inspect the current generated request before adapting this shape.

The application policy should, at minimum:

  1. reject malformed or oversized JSON;
  2. reject keys outside the endpoint field/control allowlist;
  3. allowlist sort fields and chain paths independently;
  4. validate value types and date-range order;
  5. require positive page numbers and cap _pageSize and _size;
  6. reject tenant, merchant, permission, deleted-state, and system-only fields;
  7. return the normalized JSON only after every rule passes.

The size and page limits are application policies, not TeaQL defaults. Choose them from the endpoint's latency, provider, and response-size requirements.

Chain Fields

A chain filter such as customer.name requires the corresponding child request to already exist in the generated request graph:

Q.orders()
.selectCustomer(Q.customers().selectName())
.findWithJsonExpr(validatedParams)
.filterByMerchant(ctx.getMerchant())
.comment("Query merchant orders by customer")
.purpose("Render authorized customer search results")
.executeForList(ctx);

Allowlist the full chain path. Do not accept arbitrary traversal depth or add relations merely because the caller named them.

Operator Semantics to Test

The reviewed Java helper maps value shapes to operators. Important cases include:

Input shapeReviewed interpretationRisk to test
StringCONTAIN by defaultA caller may expect exact equality.
Number or booleanEQUALNumeric conversion and invalid types.
String arrayINEmpty and very large lists.
Two-value date/time arrayRangeUnits, order, timezone, and nulls.
__is_null__ / __is_not_null__Null predicateWhether the field may be queried.
_orderByGenerated self-field orderingStable ordering and allowlisted fields.
_page / _pageSizePage windowBoundary pages and maximum size.

Use a fixed generated equality filter for security-sensitive or exact business conditions rather than relying on a string's dynamic CONTAIN behavior.

Verification

Run provider-backed integration tests with at least two tenants or merchants:

  1. A valid allowlisted filter returns only matching in-scope rows.
  2. An unknown, misspelled, forbidden, or system field is rejected.
  3. JSON that names another tenant still cannot return its rows.
  4. Oversized input, page size, IN list, and chain depth are rejected.
  5. Exact, contains, null, range, sort, first/last/empty page cases match the documented semantics.
  6. Nested chain filters cannot traverse an unselected or unapproved relation.
  7. SQL/query diagnostics show bounded execution for the supported provider.
  8. The same trusted constraints apply to any matching count query.

Record the runtime, generated API, provider/database version, test command, and query diagnostics before changing this page to verified.

Failure Modes

SymptomMost likely causeSmallest credible fix
String search is broader than expectedStrings default to CONTAIN.Use a generated exact filter or a validated exact-value shape.
Forbidden rows appearTrusted scope was omitted or tested only through JSON.Apply server-derived scope and add a cross-tenant integration test.
Filter has no effectField is unknown, misspelled, or the chain request is absent.Reject it during validation and inspect the generated request graph.
Query is slow or response is largePage, list, relation, or sort limits are missing.Enforce endpoint limits and inspect provider query diagnostics.
Generated method is missingThe example does not match the model.Read generated Q and request sources; never patch generated output.

References