Generate a Java SDK from OpenAPI / Swagger
Java SDK Overview
The Speakeasy 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 is used extensively so that problems are seen at compile time, not runtime.
- Null-safety: Primitive types are used where possible, improving compile-time null safety. For non-required and nullable fields, the
java.util.Optional
andJSONNullable
classes are used. Passing Javanull
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 are available for most SDK objects so a user can get a compile-time indication of changes to the OpenAPI document if required.
- Readability: Appropriately formatted method chaining is more understandable and maintainable.
- Discoverability: Method chaining and favorable naming strategies make life easier. For example, to build a
Person
object you callPerson.builder()
and notnew Builders.PersonFactory()
. - Convenient overloads in builders. For example, a
long
can be passed directly when the underlying field isOptional<Long>
. - The
java.util.Optional
class is used for non-required arguments. - The
JsonNullable
class is used for nullable arguments. - The Java platform
OffsetDateTime
andLocalDate
types are used fordate-time
anddate
. - A
utils
package provides shared code used by generated classes, making the generated code easier to 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 that paging is auto-handled. - Well-formatted source code to make debugging easier.
The SDK includes minimal dependencies. It requires:
- Jackson Library (opens in a new tab) to serialize and deserialize data over the wire.
- Apache HttpClient (opens in a new tab) to make HTTP requests.
- Jayway JsonPath (opens in a new tab) to support JSON path expressions in Speakeasy metadata fields in OpenAPI documents.
Java Package Structure
|-- build.gradle # more Gradle configuration|-- build-extras.gradle # custom Gradle configuration (untouched by generation updates)|-- 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└-- ...
Build Customization
The build.gradle
file should not be edited because generation updates will overwrite changes. However, customization of build.gradle
is possible:
- Additions to
build.gradle
can be made by editingbuild-extras.gradle
, which is untouched by generation updates. Note that adding plugins is not allowed inbuild-extras.gradle
, use the following technique. - Plugins can be added to
build.gradle
using the propertyadditionalPlugins
ingen.yaml
:
java:version: 0.2.0artifactID: openapi...additionalPlugins:- 'id("java")'
- Dependencies can be customized in two ways:
- Add a
dependencies
block tobuild-extras.gradle
. Note that with standard Gradle techniques, dependencies can be modified arbitrarily inbuild-extras.gradle
, including dependency exclusion, exclusion of transitive dependencies, and version modification. - Use the
additionalDependencies
property ingen.yaml
. For example, the fragment below will override thejackson-databind
version and addcommons-compress
:
- Add a
java:version: 0.2.0...addditionalDependencies:- implementation:com.fasterxml.jackson.core:jackson-databind:2.16.0- api:org.apache.commons:commons-compress:1.26.1
HTTP Client
The Java SDK HTTP client is configurable using a class implementing the following interface and is found in 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 easily replace this implementation with their own:
MyHttpClient client = new MyHttpClient();SDK sdkInstance = SDK.builder().setClient(client).build();
This gives developers using your Java SDK the flexibility to set up proxies, cookie jars, special headers, or any other low-level customization.
Serialization and Deserialization
Low-level customizations like request and response hooks or HTTPClient
-based interceptions may require the serialization and deserialization of generated objects to and from JSON.
You must use the generated custom Jackson ObjectMapper
for these actions. The ObjectMapper
is available as a singleton in the generated utils
package via JSON.getMapper()
.
Java SDK Data Types and Classes
Primitives and 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
(orjava.lang.Integer
)long
(orjava.lang.Long
)float
(orjava.lang.Float
)double
(orjava.lang.Double
)boolean
(orjava.lang.Boolean
)
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: integerformat: bigint
or
type: stringformat: bigint
The above types are mapped to java.math.BigInteger
in the generated SDK and object builders have convenient overloads that allow for passing integer values directly without wrapping them in BigInteger
.
Similarly, for unlimited-precision decimal:
type: numberformat: decimal
or
type: stringformat: decimal
The above types are mapped to java.math.BigDecimal
in the generated SDK and object builders have convenient overloads that allow passing float and double values directly without wrapping them in BigDecimal
.
Note: SDKs in other languages may choose to map to native high-precision types rather than unlimited-precision types. Check the documentation of 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
The subtypes of a non-discriminated oneOf
may be objects or primitives so a composition approach is adopted to represent a oneOf
type.
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 nullPet pet = Pet.of(cat);
Here is how a Pet
is inspected:
Pet pet = ...; // might be returned from an SDK callif (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 callif (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?");}
oneOf customization
In some circumstances the of
static factory method of a oneOf
class may need to be differentiated by a suffix to avoid type erasure. An example of this is generating from this fragment where both subtypes are arrays:
Info:oneOf:- type: arrayitems:type: integerx-speakeasy-name-override: counts- type: arrayitems:type: stringx-speakeasy-name-override: descriptions
Without accounting for this scenario the generated code would have a compile error because static factory methods Info.of(List<Long>)
and Info.of(List<String>)
conflict due to generic type erasure by the java compiler. Code generation detects this scenario and adds an of
method suffix. For the fragment above the generated static factory methods are:
ofCounts(List<Long>)
ofDescriptions(List<String>)
A suffix for of
will be selected according to this priority (and only for subtypes with potential erasure conflicts):
- use value of
x-speakeasy-name-override
if present - use speakeasy calculated name of type if present
- use speakeasy calculated name of the item type if present
- use the 1-based subtype index
Discriminated oneOf
The subtypes of a discriminated oneOf
must be objects so an interface-based inheritance approach can be adopted that provides more polymorphic convenience.
Consider this OpenAPI fragment:
Pet:oneOf:- $ref: "#/components/schemas/Cat"- $ref: "#/components/schemas/Dog"discriminator:propertyName: petTypemapping:cat: '#/components/schemas/Cat'dog: '#/components/schemas/Dog'
Here’s how a Pet
is created in Java code:
Pet cat = Cat.builder().name("Moggie").build();Pet dog = Dog.builder().name("Fido").build();
Pet
is a Java interface with a single petType()
method and Cat
and Dog
both implement that interface.
The discriminator
property should be marked as required in the oneOf
subtypes. Considering the discriminator has a constant value in each subtype of the oneOf
, it also makes sense to use a singleton enum
or a const
for the discriminator
property type. For example:
Cat:type: objectproperties:name:type: stringpetType:type: stringenum: [cat]required: [name, petType]
or
Cat:type: objectproperties:name:type: stringpetType:type: stringconst: catrequired: [name, petType]
oneOf deserialization
Speakeasy currently uses a forgiving deserialization strategy for oneOf
in that if more than one match is found in the subtypes a heuristic is used to select a best match (rather than throwing an exception). This strategy fits nicely with the auto-transformation of anyOf
to oneOf
while anyOf
implementation options are being considered.
In short the oneOf
deserialization heuristic (only applied when a JSON object is being deserialized) is:
- return the first matching subtype that has the greatest number of properties
anyOf
anyOf
is very frequently used when oneOf
is appropriate. Speakeasy is still considering specific anyOf
implementation options. For the moment anyOf
is always treated as a oneOf
. The heuristic used for oneOf
deserialization is described above and ensures compatibility with anyOf
.
Parameters
If parameters are defined in the OpenAPI document, Speakeasy will generate methods with parameters as part of the method call itself rather than as part of a separate request object.
The number of parameters defined should not exceed the maxMethodParams
value configured in the gen.yaml
file. If they do or the maxMethodParams
value is absent or set to 0
, 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 or 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 you’re using a builder, not setting the field or parameter, will mean that the default value in the OpenAPI document is used.
Bear in mind that it’s lazy-loaded (only loaded 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 most likely the 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. Fields for const
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. The 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. The best fix for this is to 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, the error
field of the response will contain the decoded error value.
Coming Soon
Support for throwing unsuccessful status codes as exceptions is 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, you have the option to run .call()
, .callAsStream()
, or .callAsStreamUnwrapped()
when using the operation builder.
- The
.call()
method will return the first page and you will have to repeatedly check for the existence of another page and retrieve it. - The
callAsStream()
method returns ajava.util.Stream
of the pages allowing you to use the convenientjava.util.Stream
API. Retrieving more pages when required and available is handled automatically. - The
callAsStreamUnwrapped()
method returns ajava.util.Stream
of the concatenated items in the lists on each page. Concatenation and page retrieval are handled automatically.
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);
Note the flatMap
calls above which concatenate the page lists. If we use callAsStreamUnwrapped
then we don’t need to do that as it’s done for you:
sdk.searchDocuments() // builder for the request.contains("simple") // parameter.minSize(200) // parameter.maxSize(400) // parameter.callAsStreamUnwrapped()// 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);
The callAsStream
and callAsStreamUnwrapped
methods throw an exception if a response page has a status code of 300 or higher. If you require a different behavior, use the call
method to manually retrieve each page.
Server-Sent Events
General Speakeasy support for server-sent events (SSE) is described here.
When an operation response has a content type of text/event-stream
, the generated response class will have an events()
method.
The events()
method can be used to traverse the event stream using a while
loop:
// we use try-with-resources to ensure closure of the underlying http connectiontry (EventStream<JsonEvent> events = response.events()) {Optional<JsonEvent> event;while ((event = events.next()).isPresent()) {processEvent(event.get());}}
The events()
method can also be used with java.util.Stream
:
// we use try-with-resources to ensure closure of the underlying HTTP connectiontry (EventStream<JsonEvent> events = response.events()) {events.stream().forEach(event -> processEvent(event));}
The events()
method can also be used to aggregate events into a list:
// closes for youList<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 that can be leveraged for tracking SDK usage amongst broader API usage. The format is as follows:
speakeasy-sdk/java {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{groupId.artifactId}}
SDKVersion
is the version of the SDK, defined ingen.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 thegroupId
andartifactId
defined ingen.yaml
.