SDKs

Java / Kotlin SDK

The taxiql-jvm library lets you execute TaxiQL queries from Java and Kotlin applications, using query classes generated from your .taxi files at build time.

Available since 1.70.0

Installation

All modules are published to Maven Central under the org.taxilang group. Add the core module plus whichever transport suits your stack.

Maven

<!-- Core interfaces (required) -->
<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-jvm-core</artifactId>
  <version>VERSION</version>
</dependency>

<!-- Pick one transport -->
<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-ktor</artifactId>
  <version>VERSION</version>
</dependency>

Gradle

implementation("org.taxilang:taxiql-jvm-core:VERSION")
implementation("org.taxilang:taxiql-transport-ktor:VERSION")

Replace VERSION with the latest release:

Releases are published to Maven Central.

Snapshot versions are published to the Orbital snapshot repository. To use a snapshot, add the repository to your pom.xml:

<repositories>
  <repository>
    <id>orbital-snapshots</id>
    <url>https://repo.orbitalhq.com/snapshot</url>
    <snapshots>
      <enabled>true</enabled>
    </snapshots>
  </repository>
</repositories>

Quick start

Given these queries in a .taxi file:

namespace com.acme.flights

query FindFlights {
    find { Flight[] } as {
        id : FlightNumber
        departs : Origin
        arrives : Destination
    }[]
}

query FindFlightDetails(flightNumber: FlightNumber) {
    find { Flight(FlightNumber == flightNumber) } as {
        id : FlightNumber
        departs : Origin
        arrives : Destination
    }
}

The taxiql-maven-plugin generates FindFlightsQuery and FindFlightDetailsQuery at build time. You can use them directly:

import com.acme.flights.FindFlightDetailsQuery
import com.acme.flights.FindFlightsQuery
import lang.taxilang.jvm.transport.ktor.KtorTaxiTransport

suspend fun main() {
    val client = KtorTaxiTransport("https://orbital.example.com")

    // Fetch all flights
    val flights = client.executeForResult(FindFlightsQuery()).getOrThrow()
    println("Found ${flights.size} flights")

    // Stream flights one by one
    client.flow(FindFlightsQuery())
        .onEach { println("${it.id}: ${it.departs}${it.arrives}") }
        .collect()

    // Fetch a single flight by number
    val flight = client.executeForResult(FindFlightDetailsQuery("QF-1")).getOrThrow()
    println("${flight.departs}${flight.arrives}")
}

Code generation

Maven plugin

Add taxiql-maven-plugin to your pom.xml. It runs automatically during generate-sources and registers the output as a compile source root, so the generated classes are available to your code without any extra steps.

<plugin>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-maven-plugin</artifactId>
  <version>VERSION</version>
  <executions>
    <execution>
      <goals>
        <goal>generate-sources</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Project structure

The plugin reads your taxi.conf to find query files. By default it looks for the conf file at src/main/taxi/taxi.conf:

# taxi.conf
name: com.acme/flights
version: 0.1.0
sourceRoot: src/

Named queries anywhere under sourceRoot are picked up and compiled. A common layout:

src/main/java          ← java source root
src/main/taxi          ← taxi source root
  src/
    schema.taxi          ← type definitions
    queries/
      FindFlights.taxi   ← named queries
      FindFlightDetails.taxi
  taxi.conf

Running mvn generate-sources (or any lifecycle phase that includes it - like mvn compile) writes a Java class per query to target/generated-sources/taxi/.

Loading types from a remote Orbital server

If your types are defined in a running Orbital instance rather than local .taxi files, point the plugin at the server and it will pull the schema automatically:

<configuration>
  <remoteUrl>https://orbital.example.com</remoteUrl>
  <!-- optional - provide authentication headers -->
  <remoteAuthHeaderValue>Bearer ${orbital.token}</remoteAuthHeaderValue>
</configuration>

The remote schema is used to resolve types referenced in your queries. The generated classes still come from the queries in your local source tree.

What gets generated

For each named query, the plugin produces a Java class that:

  • implements UnaryTaxiQlQuery<List<T>> and StreamableTaxiQlQuery<T> for find { T[] } queries, so you can either fetch the full list at once or stream results one by one with the same class
  • implements UnaryTaxiQlQuery<T> for find { T } queries
  • implements StreamableTaxiQlQuery<T> for stream { T } queries
  • has a typed constructor for each query parameter

See reference docs below for examples.

Gradle plugin

A Gradle plugin is planned. If you’d like to use one, please reach out on Slack.


Transports

Ktor

Best choice for Kotlin applications. KtorTaxiTransport uses coroutines throughout and exposes executeForResult() and flow() as the primary API.

<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-ktor</artifactId>
  <version>VERSION</version>
</dependency>
val client = KtorTaxiTransport(
    baseUrl = "https://orbital.example.com",
    authHeaderProvider = { mapOf("Authorization" to "Bearer ${token()}") }
)

KtorTaxiTransport implements AutoCloseable — use it in a use block or inject it as a long-lived singleton and call close() on shutdown.

Fetching a single result

val result = client.executeForResult(FindFlightsQuery())

result.onSuccess { flights -> println("Found ${flights.size} flights") }
result.onFailure { error -> println("Failed: ${error.message}") }

executeForResult() is a suspend function that returns Result<T>. It surfaces server-side failures without throwing, which makes it composable in coroutine pipelines.

Streaming results

client.flow(FindFlightsQuery())
    .onEach { println("${it.id}: ${it.departs}${it.arrives}") }
    .collect()

flow() returns a Kotlin Flow<T> where T is the element type. Errors during the stream (including server-reported errors) are thrown as exceptions and can be caught in the normal coroutine way.


Spring WebClient

Best choice for Spring WebFlux applications. stream() returns a Flux<T> and mono() returns a Mono<T>, making it a natural fit for reactive pipelines.

<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-webclient</artifactId>
  <version>VERSION</version>
</dependency>

Create a WebClient with the server base URL and pass it to the transport:

@Bean
fun taxiTransport(): WebClientTaxiTransport {
    val webClient = WebClient.builder()
        .baseUrl("https://orbital.example.com")
        .defaultHeader("Authorization", "Bearer ${token()}")
        .build()
    return WebClientTaxiTransport(webClient)
}

Fetching a single result

fun findFlights(): Mono<List<FindFlightsResult>> =
    transport.mono(FindFlightsQuery())

fun findFlightDetails(number: String): Mono<FindFlightDetailsResult> =
    transport.mono(FindFlightDetailsQuery(number))

Streaming results

fun streamFlights(): Flux<FindFlightsResult> =
    transport.stream(FindFlightsQuery())

Errors during the stream surface as Flux.error / Mono.error and can be handled with .onErrorMap() or .doOnError().


Java HttpClient

Zero additional dependencies — uses the java.net.http.HttpClient built into Java 11+. Best for Java applications or anywhere you want to minimise the dependency footprint.

<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-http-client</artifactId>
  <version>VERSION</version>
</dependency>
var transport = new JavaHttpClientTaxiTransport(
    "https://orbital.example.com",
    () -> Map.of("Authorization", "Bearer " + token())
);

Fetching a single result

// Non-blocking
transport.execute(new FindFlightsQuery())
    .thenAccept(flights ->
        System.out.println("Found " + flights.size() + " flights")
    );

// Blocking
var flights = transport.execute(new FindFlightsQuery()).get();

Streaming results

stream() returns a Publisher<T> compatible with Project Reactor, RxJava, and any other Reactive Streams library:

transport.stream(new FindFlightsQuery())
    .subscribe(new Subscriber<>() {
        public void onSubscribe(Subscription s) { s.request(Long.MAX_VALUE); }
        public void onNext(FindFlightsResult f) { System.out.println(f.getId()); }
        public void onError(Throwable t) { t.printStackTrace(); }
        public void onComplete() { System.out.println("Done"); }
    });

Parameterized queries

When a query declares parameters, the generated class has a constructor that accepts them:

query FindFlightDetails(flightNumber: FlightNumber) {
    find { Flight(FlightNumber == flightNumber) } as { ... }
}
// Kotlin
val flight = client.executeForResult(FindFlightDetailsQuery("QF-1")).getOrThrow()
// Java
var flight = transport.execute(new FindFlightDetailsQuery("QF-1")).get();

Parameter types follow the Taxi type system. For type aliases wrapping primitives (like FlightNumber inherits String), the constructor parameter is the underlying Java type (String in this case), annotated with @DataType carrying the Taxi type name. This lets frameworks that understand Taxi semantics do type-safe wiring automatically.


Error handling

Query failures

When the server rejects a query — either with a non-2xx HTTP response or by emitting an event:error SSE event mid-stream — the SDK surfaces a QueryFailureException containing the server’s error message.

With executeForResult() (Ktor), failures come back as Result.failure rather than exceptions:

val result = client.executeForResult(FindFlightsQuery())
result
    .onSuccess { flights -> println("Found ${flights.size} flights") }
    .onFailure { error ->
        if (error is QueryFailureException)
            println("Query failed: ${error.failure.message}")
    }

With Spring WebClient, they surface as Mono.error / Flux.error:

transport.mono(FindFlightsQuery())
    .onErrorMap(QueryFailureException::class.java) { e ->
        MyApplicationException("Query failed: ${e.failure.message}")
    }

With CompletableFuture (Ktor’s execute() or Java HttpClient), the exception is wrapped in a standard ExecutionException:

try {
    val flights = transport.execute(FindFlightsQuery()).get()
} catch (e: ExecutionException) {
    val cause = e.cause
    if (cause is QueryFailureException)
        println("Query failed: ${cause.failure.message}")
}

SSE stream errors

The server can signal an error mid-stream using an SSE error event:

event:error
data:{"message":"No data sources were found that can return Flight[]", ...}

The SDK detects this and terminates the stream with a QueryFailureException, using whatever error channel is natural for the transport. Any results emitted before the error are delivered normally.

With Ktor flow(), catch it like any other exception:

try {
    client.flow(FindFlightsQuery())
        .onEach { println(it.id) }
        .collect()
} catch (e: QueryFailureException) {
    println("Stream failed: ${e.failure.message}")
}

With Spring WebClient’s Flux, handle it with .doOnError():

transport.stream(FindFlightsQuery())
    .doOnError(QueryFailureException::class.java) { e ->
        println("Stream failed: ${e.failure.message}")
    }
    .subscribe()

Jackson configuration

All transports use Jackson for JSON deserialization. The default ObjectMapper is Kotlin-aware (via jackson-module-kotlin). Pass a custom objectMapper to the constructor if you need specific serialization settings such as custom date formats or naming strategies.


Reference

The following classes are used by the SDK. They’re generated by code-gen tools, such as the Maven plugin, rather than intended to be written by hand.

TaxiTransport

The core interface implemented by all three transports:

interface TaxiTransport {
    fun <T> stream(query: StreamableTaxiQlQuery<T>): Publisher<T>
    fun <T> execute(query: UnaryTaxiQlQuery<T>): CompletableFuture<T>
}

KtorTaxiTransport and WebClientTaxiTransport extend this with idiomatic extras (flow(), executeForResult(), mono()) — prefer those in Kotlin and Spring code respectively.

UnaryTaxiQlQuery

Used for queries that return a single result or a complete list in one response:

// Single object
class FindFlightQuery(private val flightId: String) : UnaryTaxiQlQuery<Flight> {
    override val taxiQl = """
        query FindFlight(flightId: FlightId) {
            find { Flight( FlightId == flightId ) }
        }
    """.trimIndent()
    override val arguments = mapOf("flightId" to flightId)
    override val responseType = object : TypeReference<Flight>() {}
}

// Collection
class FindAllFlightsQuery : UnaryTaxiQlQuery<List<Flight>> {
    override val taxiQl = "find { Flight[] }"
    override val arguments = emptyMap<String, Any?>()
    override val responseType = object : TypeReference<List<Flight>>() {}
}

StreamableTaxiQlQuery

Used for queries that stream results one element at a time:

class LiveFlightsQuery : StreamableTaxiQlQuery<Flight> {
    override val taxiQl = "stream { Flight[] }"
    override val arguments = emptyMap<String, Any?>()
    override val streamType = object : TypeReference<Flight>() {}
}

A find { T[] } query can implement both interfaces on the same class, allowing the caller to choose between receiving the full list at once or streaming element by element:

class FindAllFlightsQuery : UnaryTaxiQlQuery<List<Flight>>, StreamableTaxiQlQuery<Flight> {
    override val taxiQl = "find { Flight[] }"
    override val arguments = emptyMap<String, Any?>()
    override val responseType = object : TypeReference<List<Flight>>() {}
    override val streamType = object : TypeReference<Flight>() {}
}

The taxiql-maven-plugin generates this dual-interface pattern automatically for all find { T[] } queries.

Previous
Linter
Next
Adopting semantic types