Generate a Java SDK from OpenAPI / Swagger

Java SDK Overview

Speakeasy's Java SDK is designed to be easy to use and debug. This includes generating strongly typed classes that enforce required fields and other validations to ensure the messages sent are correct. This allows for a tighter development cycle so your API consumers can spend less time developing solutions using your API.

The core features of the SDK include:

  • Type-safety - strong-typing used extensively so that problems are seen at compile-time not run-time
  • Null-safety - primitive types used where possible improving compile-time null-safety, java.util.Optional and JSONNullable classes used for non-required and nullable fields. Passing Java null arguments will provoke an exception.
  • Builders and method chaining for all SDK objects. For example, to create a Person object:

Person person = Person.builder()
.firstName("Albert")
.lastName("Einstein")
.dateOfBirth(LocalDate.parse("1879-03-14"))
.build();

  • All-field constructors available for nearly all SDK objects (so a user can get compile-time indication of changes to the OpenAPI document if required)
  • Readability - appropriately formatted method chaining is more comprehensible and maintainable
  • Discoverability - method chaining and favourable naming strategies make life easier (for example to build a Person object you call Person.builder(), not new Builders.PersonFactory())
  • Convenient overloads in builders (for example so that a long can be passed directly when the underlying field is Optional<Long>)
  • java.util.Optional used for non-required arguments
  • JsonNullable used for nullable arguments
  • java platform OffsetDateTime and LocalDate types used for date-time and date
  • A utils package that provides shared code used by generated classes, making the generated code easier follow
  • Authentication support for OAuth flows and other standard security mechanisms
  • Custom enum types using string or integer values
  • Pagination support including the option to return java.util.Stream so paging is auto-handled
  • Well-formatted source code to make debugging easier

The SDK includes minimal dependencies. It requires

Java Package Structure

lib-structure.yaml

|-- build.gradle # more gradle configuration
|-- build # directory that will contain built artifacts
| └-- ...
|-- src # source code directory
| └-- main # main source code directory
| └-- {SDK Package Name} # sub-directories to the SDK package namespace
| |-- SDK.java # primary SDK class
| |-- ... # additional sub-SDK classes
| |-- models # package for model-related files
| | |-- operations # package for request/response operational models
| | | └-- ...
| | └-- shared # package for shared component models
| | └-- ...
| └-- utils # package for shared utility classes
| └-- ...
|-- docs # Markdown files for the SDK's documentation
|-- gradlew # gradle shellscript to build/install the SDK
|-- gradlew.bat # gradle batch file to build/install the SDK
|-- settings.gradle # provided gradle settings
|-- gradle
| └-- ... # other gradle related files
└-- ...

HTTP Client

The Java SDK HTTP client is completely configurable using a class implementing the following interface (found under the util package of the generated code):


public interface HTTPClient {
public HTTPResponse<byte[]> send(HTTPRequest request)
throws IOException, InterruptedException, URISyntaxException
}

A default implementation is provided based on java.net.HttpClient. Any developer using the SDK can replace this implementation their own implementation very easily:


MyHttpClient client = new MyHttpClient();
SDK sdkInstance = SDK.builder().setClient(client).build();

This gives them the flexibility to setup proxies, cookie jars, special headers, or any other low-level customization.

Java SDK Data Types & Classes

Primitives, native types

Where possible the Java SDK uses native types from the language. Primitives are used wherever possible for increased null-safety. For example:

  • java.lang.String
  • java.time.OffsetDateTime
  • java.time.LocalDate
  • java.math.BigInteger
  • java.math.BigDecimal
  • int (or java.lang.Integer)
  • long (or java.lang.Long)
  • float (or java.lang.Float)
  • double (or java.lang.Double)
  • boolean (or java.lang.Boolean)
  • etc...

Unlimited-precision Numerics

Using high precision decimal or integer types is crucial in certain applications such as code manipulating monetary amounts and in situations where overflow, underflow, or truncation caused by precision loss can lead to significant incidents.

To mark a field as an unlimited precision integer you can use:


type: integer
format: bigint

or


type: string
format: bigint

The above types are mapped to java.math.BigInteger in the generated SDK and object builders have convenient overloads that allow passing integer values directly without wrapping with BigInteger.

Similarly, for unlimited precision decimal:


type: number
format: decimal

or


type: string
format: decimal

The above types are mapped to java.math.BigDecimal in the generated SDK in the generated SDK and object builders have convenient overloads that allow passing float/double values directly without wrapping with BigDecimal.

Note: SDKs in other languages may choose to map to native high precision types rather than unlimited precision types. Check the documentation for the language you are interested in.

Union types (oneOf)

Support for polymorphic types is critical to most production applications. In OpenAPI, these types are defined using the oneOf keyword. Non-discriminated oneOf types are supported.

Consider this OpenAPI fragment:


Pet:
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"

Here's how a Pet is created in java code:


Cat cat = ...;
Dog dog = ...;
// Pet.of only accepts Cat or Dog types, and throws if passed null
Pet pet = Pet.of(cat);

Here's how a Pet is inspected:


Pet pet = ...; // might be returned from an SDK call
if (pet.value() instanceof Cat) {
Cat cat = (Cat) pet.value();
// do something with the cat
} else if (pet.value() instanceof Dog) {
Dog dog = (Dog) pet.value();
// do something with the dog
} else {
throw new RuntimeException("unexpected value, openapi definition has changed?");
}

If developing with Java 14+ then you can make use of pattern matching language features:


Pet pet = ...; // might be returned from an SDK call
if (pet.value() instanceof Cat cat) {
// do something with the cat
} else if (pet.value() instanceof Dog dog) {
// do something with the dog
} else {
throw new RuntimeException("unexpected value, openapi definition has changed?");
}

Parameters

If configured, the Java SDK will generate methods with parameters as part of the method call itself rather than as part of a separate request object. This will be done for up to maxMethodParams parameters, that can be set in the gen.yaml file.

If the maxMethodParams configuration option is absent or set to zero, all generated methods require a single request object that contains all the parameters that may be used to call an endpoint in the API.

Default values

The default keyword in the OpenAPI specification allows a user to omit a field/parameter and it will be set with a given default value. Default values are represented in the Java SDK with java.util.Optional wrappers. Passing Optional.empty() (or if using a builder, not setting the field/parameter) will mean that the default value in the OpenAPI document is used. Bear in mind that it is lazy-loaded (only once) and that if the default value is not valid for the given type (say default: abc is specified for type: integer) an IllegalArgumentException will be thrown. If you encounter this situation, you have two options:

  • regenerate the SDK with a fixed default value in the OpenAPI document

or

  • set the value of the field explicitly (so that the once-only lazy-load of the default value never happens). This technique is the most likely immediate workaround for a user that does not own the SDK repository

Constant values

The const keyword in the OpenAPI specification ensures that a field is essentially read-only and that its value will be the specified constant. const fields will not be settable in all-parameter constructors or builders, their value will be set internally. However, const fields are readable in terms of object getters. const values are lazy-loaded once-only (like default values). If the const value is not valid for the given type then an IllegalArgumentException will be thrown and the best fix is:

  • regenerate the SDK with a fixed const value in the OpenAPI document

Errors

To handle errors in the Java SDK, you need to check the status code of the response. If it is an error response, then the error field of the response will be set the decoded error value.

Info Icon

Coming Soon

Support for throwing non-successful status codes as exceptions coming soon.

Pagination and java.util.Stream

Enabling pagination for an operation in your API is described here.

If pagination is enabled for an operation then when using the operation builder you have the option to run .call() or .callAsStream().

  • .call() will return the first page and you will have to repeatedly check for the existence of another page and retrieve it.
  • callAsStream() returns a java.util.Stream of the pages allowing you to use the convenient java.util.Stream API and not be concerned with handling paging yourself.

Below is an example of callAsStream().


SDK sdk = SDK.builder() ... ;
sdk.searchDocuments() // builder for the request
.contains("simple") // parameter
.minSize(200) // parameter
.maxSize(400) // parameter
.callAsStream() // returns Stream<DocumentsPageResponse>
.flatMap(x -> x.res() // returns Optional<DocumentsPage>
.stream()
.flatMap(y -> y.documents().stream()))
// we are now dealing with a Stream<Document>
.filter(document -> "fiction".equals(document.category())
.limit(200) // no more than 200 documents
.map(document -> document.name())
.forEach(System.out::println);

callAsStream throws when a response page has a status code of >=300. If you desire different behaviour then use call to retrieve each page yourself.

Server-sent events

General Speakeasy support for server-sent events (SSE) is described here (opens in a new tab).

When an operation response has a content type of text/event-stream the generated response class will have an events() method that can be used as follows:

The event stream can be traversed using a while loop:


// we use try-with-resources to ensure closure of the underlying http connection
try (EventStream<JsonEvent> events = response.events()) {
Optional<JsonEvent> event;
while ((event = events.next()).isPresent()) {
processEvent(event.get());
}
}

or a java.util.Stream version


// we use try-with-resources to ensure closure of the underlying http connection
try (EventStream<JsonEvent> events = response.events()) {
events.stream().forEach(event -> processEvent(event));
}

or aggregate events to a list:


// closes for you
List<JsonEvent> events = response.events().toList();
events.forEach(event -> processEvent(event));

User Agent Strings

The Java SDK will include a user agent (opens in a new tab) string in all requests. This can be leveraged for tracking SDK usage amongst broader API usage. The format is as follows:


speakeasy-sdk/java {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{groupId.artifactId}}

Where

  • SDKVersion is the version of the SDK, defined in gen.yaml and released
  • GenVersion is the version of the Speakeasy generator
  • DocVersion is the version of the OpenAPI document
  • groupId.artifactId is the concatenation of the groupId and artifactId defined in gen.yaml