Java SDK
The official Java SDK for JSONQL. Configure a connection, register the adapter, and your API instantly supports dynamic filtering, sorting, pagination, field selection, and relationships.
| Group | org.jsonql |
| Artifact | jsonql-java |
| Java | 17+ |
| License | MIT |
| Spec | JSONQL v1.1 |
Install
Section titled “Install”<dependency> <groupId>org.jsonql</groupId> <artifactId>jsonql-java</artifactId> <version>1.1.0</version></dependency>Gradle
Section titled “Gradle”implementation("org.jsonql:jsonql-java:1.1.0")Configure & Use
Section titled “Configure & Use”Spring Boot
Section titled “Spring Boot”import org.jsonql.adapter.AdapterOptions;import org.jsonql.adapter.spring.SpringAdapter;import org.springframework.web.bind.annotation.*;
import javax.sql.DataSource;
@RestController@RequestMapping("/api")public class JsonQLController {
private final SpringAdapter adapter;
public JsonQLController(DataSource ds) { AdapterOptions opts = new AdapterOptions() .connectionSupplier(ds::getConnection);
this.adapter = new SpringAdapter(opts); }
@PostMapping("/{table}") public Object query(@PathVariable String table, @RequestBody String body) { return adapter.handleQuery(table, body); }
@PostMapping("/{table}/create") public Object create(@PathVariable String table, @RequestBody String body) { return adapter.handleCreate(table, body); }
@PatchMapping("/{table}") public Object update(@PathVariable String table, @RequestBody String body) { return adapter.handleUpdate(table, body); }
@DeleteMapping("/{table}") public Object delete(@PathVariable String table, @RequestBody String body) { return adapter.handleDelete(table, body); }}That’s it. Spring Boot auto-wires your DataSource. All JSONQL operations route through the adapter — no query-building needed, no custom SQL.
Jakarta Servlet (no Spring)
Section titled “Jakarta Servlet (no Spring)”import org.jsonql.adapter.AdapterOptions;import org.jsonql.adapter.servlet.JsonQLServlet;
AdapterOptions opts = new AdapterOptions() .connectionSupplier(() -> DriverManager.getConnection(url, user, pass));
// Register servlet at /api/*servletContext.addServlet("jsonql", new JsonQLServlet(opts)) .addMapping("/api/*");MongoDB
Section titled “MongoDB”For MongoDB-backed APIs, use the *MongoAdapter classes. They take a
MongoDriverInterface (e.g. MongoDriver wrapping a MongoDatabase) and
produce the same {meta, data} response contract:
import org.jsonql.MongoDriver;import org.jsonql.adapter.MongoAdapterOptions;import org.jsonql.adapter.spring.SpringMongoAdapter;import com.mongodb.client.MongoClients;import com.mongodb.client.MongoDatabase;
@RestController@RequestMapping("/api")public class JsonQLMongoController {
private final SpringMongoAdapter adapter;
public JsonQLMongoController() { MongoClient client = MongoClients.create("mongodb://localhost:27017"); MongoDatabase db = client.getDatabase("mydb"); MongoAdapterOptions opts = new MongoAdapterOptions() .driver(new MongoDriver(client, db));
this.adapter = new SpringMongoAdapter(opts); }
@PostMapping("/{collection}") public Object query(@PathVariable String collection, @RequestBody Map<String, Object> body) { var result = adapter.handle(body, "POST", collection); return ResponseEntity.status(result.status).body(result.body); }}The Jakarta variant is JakartaMongoAdapter, constructed the same way.
What Your Clients Can Do
Section titled “What Your Clients Can Do”Every query is a JSON POST to /api/{table}:
# Select specific fields, filter, sort, paginatecurl -X POST http://localhost:8080/api/users -H 'Content-Type: application/json' -d '{ "fields": ["id", "name", "email"], "where": { "status": { "eq": "active" } }, "sort": ["-created_at"], "limit": 20}'# → { "data": [{ "id": 1, "name": "Alice", "email": "alice@co.com" }, ...] }# Complex filterscurl -X POST http://localhost:8080/api/products -d '{ "where": { "and": [ { "price": { "gte": 10, "lte": 100 } }, { "category": { "in": ["electronics", "books"] } } ] }, "sort": ["price"], "limit": 50}'# Include related datacurl -X POST http://localhost:8080/api/users -d '{ "fields": ["id", "name"], "include": { "posts": { "fields": ["title", "created_at"], "limit": 5 } }}'# Aggregation & groupBycurl -X POST http://localhost:8080/api/orders -d '{ "aggregate": { "total": { "fn": "sum", "field": "amount" } }, "groupBy": ["status"]}'CRUD mutations:
# Createcurl -X POST http://localhost:8080/api/users/create \ -d '{ "data": { "name": "Bob", "email": "bob@co.com" } }'
# Updatecurl -X PATCH http://localhost:8080/api/users \ -d '{ "patch": { "status": "inactive" }, "where": { "id": { "eq": 1 } } }'
# Deletecurl -X DELETE http://localhost:8080/api/users \ -d '{ "where": { "id": { "eq": 1 } } }'Adding Schema (Optional)
Section titled “Adding Schema (Optional)”Without schema, all columns are queryable. With schema, you control field exposure and enable relationships:
import org.jsonql.schema.JsonQLSchema;
JsonQLSchema schema = JsonQLSchema.builder() .table("users", t -> t .field("id", "number") .field("name", "string") .field("email", "string") .relation("posts", r -> r.hasMany("posts", "user_id")) ) .build();
AdapterOptions opts = new AdapterOptions() .connectionSupplier(ds::getConnection) .schema(schema);Lifecycle Hooks
Section titled “Lifecycle Hooks”Inject tenant isolation, audit logging, or authorization:
AdapterOptions opts = new AdapterOptions() .connectionSupplier(ds::getConnection) .beforeQuery((query, table) -> { // Inject tenant filter into every query query.addWhere("tenant_id", "eq", tenantId); }) .afterQuery((result, table) -> { auditLog.record("query", table, result.getRowCount()); });Supported Databases
Section titled “Supported Databases”| Database | Dialect | JDBC Driver |
|---|---|---|
| PostgreSQL | postgres | org.postgresql:postgresql |
| MySQL | mysql | com.mysql:mysql-connector-j |
| SQLite | sqlite | org.xerial:sqlite-jdbc |
| MSSQL | mssql | com.microsoft.sqlserver:mssql-jdbc |
| MongoDB | mongodb | org.mongodb:mongodb-driver-sync |
Dialect is auto-detected from JDBC URL; explicit setting is optional:
new AdapterOptions() .connectionSupplier(ds::getConnection) .dialect("postgres"); // Only needed if auto-detect failsError Handling
Section titled “Error Handling”All errors extend JsonQLException with a getErrorCode() method:
graph TD E["JsonQLException"] --> P["JsonQLParseException<br/>(PARSE_ERROR)"] E --> V["JsonQLValidationException<br/>(VALIDATION_ERROR)"] E --> T["JsonQLTranspileException<br/>(TRANSPILE_ERROR)"] E --> X["JsonQLExecutionException<br/>(EXECUTION_ERROR)"]try { adapter.handleQuery(table, body);} catch (JsonQLException e) { System.out.println(e.getErrorCode()); // "VALIDATION_ERROR"}Adapter error responses:
{ "error": "Field 'secret' is not allowed", "error_code": "VALIDATION_ERROR" }Advanced: Engine (Direct Use)
Section titled “Advanced: Engine (Direct Use)”For custom pipelines outside the Spring adapter:
import org.jsonql.JsonQLFactory;import org.jsonql.JsonQLResult;
JsonQLFactory factory = JsonQLFactory.builder() .dialect("postgres") .connection(ds.getConnection()) .schema(schema) // optional .build();
JsonQLResult result = factory.execute("users", queryJson);// result.getData() → List<Map<String, Object>>// result.isMutation() → booleanAdvanced: Query Builder
Section titled “Advanced: Query Builder”For server-side programmatic query construction:
import org.jsonql.builder.QueryBuilder;
String query = QueryBuilder.from("users") .select("id", "name", "email") .where("status", "eq", "active") .orderBy("-created_at") .limit(10) .toJson();Core API
Section titled “Core API”| Class | Purpose |
|---|---|
AdapterOptions | Configure adapter: connection, schema, hooks, dialect |
SpringAdapter | Spring Boot request handler |
JsonQLFactory | Manual transpile-and-execute pipeline |
JsonQLParser | Parse & validate incoming JSON |
SQLTranspiler | Convert parsed query → SQL + params |
Hydrator | Convert flat rows → nested POJOs/maps |
QueryBuilder | Fluent query construction (advanced) |
Compliance
Section titled “Compliance”135/135 tests passing across all configurations:
| Adapter | PostgreSQL | MySQL | SQLite | MSSQL | MongoDB |
|---|---|---|---|---|---|
| Spring | ✅ | ✅ | ✅ | ✅ | ✅ |
| Jakarta | ✅ | ✅ | ✅ | ✅ | ✅ |
Development
Section titled “Development”mvn test # Run all tests (JUnit 4 + H2)mvn verify # Full verification including integration testsArchitecture
Section titled “Architecture”The SDK is organised into 12 canonical modules. A request flows through them in order:
parser → validator → transpiler → driver → drivers → hydrator ↑ factory wires it together ↑ builder is an alternative input| Module | File | Purpose |
|---|---|---|
| parser | JsonQLParser.java | Tokenise & validate incoming JSON, produce an AST |
| validator | embedded in parser & schema | Schema & permission checks against the AST |
| transpiler | SQLTranspiler.java | AST → parameterised SQL for the active dialect |
| mongo_transpiler | MongoTranspiler.java | AST → MongoDB filter & aggregation pipelines |
| dialect | dialect/*.java | Per-flavour quoting & parameter markers (Postgres $1, MySQL/SQLite ?, MSSQL @p1) |
| driver | JsonQLDriver.java | Interface: query(), execute(), close() |
| drivers | per JDBC driver (Postgres, MySQL, SQLite, MSSQL) + mongodb | Concrete backends |
| hydrator | hydrator/ | Flatten join rows back into nested POJOs/maps |
| factory | JsonQLFactory.java (JsonQLFactory.builder()) | One-call wiring of parser + transpiler + driver + hydrator |
| builder | QueryBuilder.java | Fluent programmatic query construction |
| schema | schema/ (JsonQLSchema.builder()) | Optional schema loader |
| adapters | adapter/{spring,servlet} | Framework integrations |
| errors | JsonQLException and subclasses | Error hierarchy with getErrorCode() |
For most apps you only touch adapters at install time and the factory for advanced wiring. Everything else is automatic.