Installation¶
Requirements¶
- JDK 17 or newer to run. jev4k is compiled to Java 17 bytecode, so it embeds in applications on 17, 21, 25, or anything later. Building jev4k itself uses JDK 25; Gradle's toolchain support downloads it if it's missing.
- Kotlin 2.4 or later.
- A TypeSafe API key.
jev4k is built on the Ktor client (CIO engine), kotlinx.serialization, and kotlinx.coroutines. Ktor core,
kotlinx.serialization-json and kotlinx.coroutines are exposed as api dependencies, because their types (JsonElement,
HttpClientEngine, suspend functions) appear in jev4k's public API.
Embedding in an application¶
jev4k is a library meant to be embedded in an application, so it keeps out of the host's way.
What it puts on your classpath.¶
Four compile dependencies (ktor-client-core,
kotlinx-serialization-json, kotlinx-coroutines-core, kotlin-stdlib) and three runtime ones (ktor-client-cio,
ktor-client-content-negotiation, ktor-serialization-kotlinx-json). Nothing else: no test
framework, no logging backend.
Logging¶
jev4k never logs. It writes nothing to stdout or stderr, installs no Ktor Logging plugin, and
ships no SLF4J binding, so it can't interfere with your logging setup. slf4j-api reaches the classpath through
Ktor, not jev4k; supply your own binding if you want Ktor's own output.
Your own engine¶
Pass one through engine and jev4k uses
it instead of CIO. Closing a JevClient never closes an engine you supplied, so several clients can share one.
If you do supply an engine, CIO can be dropped:
dependencies {
implementation("com.pambrose:jev4k:0.1.0") {
// Only if you pass your own engine: drops ktor-client-cio and ktor-network-tls. ktor-network
// itself stays, because ktor-client-core depends on it through ktor-http-cio.
exclude(group = "io.ktor", module = "ktor-client-cio-jvm")
}
implementation("io.ktor:ktor-client-okhttp:3.6.0")
}
From Java¶
jev4k is a Kotlin library, but the inline builder DSL works from Java through jev.getBlocking():
JevClient jev = new JevClient(builder -> {
builder.setApiKey(System.getenv("TYPESAFE_API_KEY"));
return Unit.INSTANCE;
});
JevResult r = jev.getBlocking().query("The payout failed again and I need this fixed today.", null, qb -> {
qb.noul("urgent", "Does this message convey urgency?");
return Unit.INSTANCE;
});
double urgency = r.noul("urgent").getNoul();
Two Kotlin features don't cross to Java: property delegates, which a typed JevQuery is built from, and
inline reified functions, which the Kotlin compiler emits as synthetic members that javac can't resolve. So
four things are out of reach from Java:
- Typed
JevQueryobjects can't be declared. One declared in Kotlin can still be passed toask. @Serializablestates. A state must be aStringor aJsonElement; the reifiedquery,askandjsonEntryoverloads are hidden rather than compiling into a runtime failure.- Enum Choices through the DSL.
QueryBuilder.choice<E>()is reified andenumChoiceRefisinternal, so there's no route to one. Build aChoiceQuestionwith the option keys you want and add it withQueryBuilder.question(id, question)instead. JevResult.enumChoice<E>(id)is reified too. Read that answer withresult.choice(id), which is keyed by option string.
Everything else is callable: evaluate, models, the inline noul, choice and score builders, the other
result accessors, and enums implementing JevOption.
Module name¶
The jar declares Automatic-Module-Name: com.pambrose.jev4k for JPMS builds.
Java version¶
The class files are Java 17 (org.gradle.jvm.version = 17 in the published metadata), and the
compiler is held to the Java 17 API, so nothing newer can slip in.
Threads¶
A JevClient is immutable once built and safe to share across coroutines. jev.blocking wraps the
suspend calls in runBlocking, so call it from ordinary threads, never from inside a coroutine.
Adding the dependency¶
jev4k is published to Maven Central as com.pambrose:jev4k.
That single dependency brings the Ktor client, kotlinx.serialization and kotlinx.coroutines with it; see what it puts on your classpath for the full set, and your own engine if you'd rather not ship CIO.
Building from source¶
Contributors, and anyone who wants a build before the next release reaches Central, can build jev4k from a checkout:
# From a checkout of the jev4k repository (builds with JDK 25; Gradle downloads it if missing)
./gradlew build
Using an unreleased build from another project¶
The simplest way to depend on a checkout rather than a published artifact is a Gradle composite build. Include the jev4k checkout in your project's settings:
// settings.gradle.kts of your project: build jev4k from a sibling checkout
includeBuild("../jev4k")
Then depend on it by its coordinates, which Gradle substitutes with the included build:
// build.gradle.kts of your project
plugins {
kotlin("jvm") version "2.4.20"
kotlin("plugin.serialization") version "2.4.20" // only needed for @Serializable state
}
dependencies {
implementation("com.pambrose:jev4k:0.1.1") // the version in the checkout's gradle.properties
}
kotlin {
jvmToolchain(17) // jev4k targets Java 17, so anything 17 or newer works
}
The plugin.serialization line is needed only if you pass your own @Serializable classes as
state.
API key¶
The client reads the key from the TYPESAFE_API_KEY environment variable:
You can also set it in code; see Configuration. Keep API keys server-side: don't ship them in a browser or mobile app.
Checking your setup¶
The repository includes a runnable example that uses both DSL styles against the live API:
export TYPESAFE_API_KEY=ts-...
make example # runs src/test/kotlin/com/pambrose/jev4k/examples/TriageExample.kt
Next: the Quick Start.