Generate a Java SDK from your OpenAPI spec
Octri reads your OpenAPI document and writes a Java client with typed models, publishes it to Maven Central under a groupId you control, and rebuilds it every time the spec changes.
<dependency>
<groupId>com.acme</groupId>
<artifactId>api-client</artifactId>
<version>1.0.0</version>
</dependency>The client they actually get
What your users write once the Java package is installed.
Two call-site shapes come out of the same spec. class-namespaced is the default: a client object whose operations are grouped by resource, which is the surface every widely used SDK ships and the one your own reference docs already assume. functions is the override: no client object, one configure call, and each operation called directly. It is a single setting, per language, and nothing else about the client changes with it.
Requests are builders either way, so a new optional parameter never breaks a call site that already compiles.
Coordinates are groupId:artifactId, and Maven Central verifies that the groupId is a domain you control. That check is a one-off, and it is the part of a first Java release that takes the longest.
var config = new SdkConfig.ClientConfig();
config.baseUrl = "https://api.acme.com/v1";
var auth = new SdkConfig.ClientAuthConfig();
auth.bearer = System.getenv("ACME_TOKEN");
config.auth = auth;
var acme = new Acme(config);
// Namespaces are final fields on the client. The callback returns false to stop.
acme.users.listPaginated(
Users.ListUsersRequest.builder().limit(100L).build(),
user -> {
System.out.println(user.id);
return true;
});SdkConfig.configureClient(config);
Users.listUsersPaginated(
Users.ListUsersRequest.builder().limit(100L).build(),
user -> {
System.out.println(user.id);
return true;
});The package at a glance
What a developer checks before adding the Java package to a project.
- Runs on
- Java 17+
- Transport
- java.net.http.HttpClient
- Runtime dependencies
- Jackson (with JSR-310) and the JSR-305 annotations
- Models
- Final classes with builders and public fields
- Pagination
- Predicate callback that returns false to stop
- Ships to
- Maven Central, for Maven and Gradle alike
When a call fails
The errors are unchecked, so nothing forces a throws clause onto your users’ signatures, and each common status is its own subclass for a targeted catch.
try {
var invoice = acme.invoices.create(
Invoices.CreateInvoiceRequest.builder()
.customerId("cus_123")
.amountCents(4900L)
.build());
} catch (SdkRateLimitError e) {
// 429 after the built-in retries. Reschedule the job.
} catch (SdkHttpError e) {
log.error("acme failed: status={} request={}", e.statusCode, e.requestId);
}What you decide for Java
Java exposes no HTTP engine setting. Kotlin offers okhttp and ktor; Java uses java.net.http.HttpClient.
Client style
class-namespaced (default), class, namespaced, functionsA client object with a field per resource, which is what a Java caller expects. The functions style maps awkwardly onto a language with no free functions, so it is an override you are unlikely to want here.
Argument style
object (default), positionalThe object style is a request builder, the JVM convention for anything with optional fields. Positional becomes an overload set that grows with your spec.
Doc comments
full (default), minimalFull carries your spec’s prose into the Javadoc an IDE shows on hover.
Method naming and namespacing work the same in every language: operations are grouped by your spec’s tags or by path, and the namespace word is dropped from the method name unless you ask to keep the operationId whole.
Maven Central verifies your groupId
The groupId has to be a domain you actually control, and Sonatype checks it before your first release. com.acme means proving you own acme.com. Start that verification before you plan the release, because it is the one step in a Java launch that is out of your hands.
com.acme: api-client- Not possible: Publish under a domain you do not own
- Not possible: Delete or replace a release
- Still possible: Relocate users to new coordinates
The same in every generated client
The parts nobody wants to hand-write, and the parts a hand-written client usually skips.
- Retries with exponential backoff and jitter, on the statuses that mean "try again", with a per-attempt timeout.
- Idempotency keys on the methods that need them, so a retried write does not become two.
- Auth wired in from your spec’s security scheme, read from wherever your users keep secrets.
- Typed errors split by cause: a failed constraint, an HTTP status, a network failure and a timeout are four different things.
- Pagination that follows whichever contract your spec declares, cursor, offset, page number or next URL, without prefetching the collection.
- Server-sent events as a native stream, for the operations that stream.
- Error reporting to Octri’s monitoring, pre-wired and switched off until a consumer opts in.
Point it at your spec
Upload an OpenAPI or Swagger document, paste a URL, or connect the repository it lives in so a merge to main updates it.
Read the audit
The spec is scored out of 10 on what a generator can do with it, with every missing schema and colliding operationId named. 8 of the fourteen rules carry a button that writes the fix.
Shape the client
Client style, argument style, namespacing, auth, retries and pagination, set once for the project and overridden per language or per endpoint.
Generate and read it
The package is generated, compiled and handed to you as source you can read before anyone installs it.
Publish, then keep publishing
Release to the registry under your own account. Every later spec change regenerates the client, and the version number comes from your own document.
Or in another language
One spec, ten languages. The other nine are generated from the same document, with the same settings, at the same time.
Questions
Either. The package is published to Maven Central, and both build tools resolve from it. The README carries the coordinates in both forms.