Speakeasy generates Java SDKs that integrate naturally with existing Java ecosystems, following established conventions for consistency with hand-written libraries. The generated code provides full IDE support, compile-time validation, and seamless integration with standard tooling.
Design principles:
Native Java ergonomics: Code generation leverages Java’s type system, generics, and method chaining to create APIs that feel natural to Java developers. Builder patterns, fluent interfaces, and standard library types eliminate the need to learn framework-specific abstractions
Comprehensive type safety: Strong typing catches API contract violations at compile time, while JSR-305/Jakarta nullability annotations provide rich IDE warnings and autocomplete derived directly from your OpenAPI specification
Flexible concurrency models: Synchronous execution by default supports traditional blocking patterns, while .async() mode provides CompletableFuture<T> and reactive streams support for non-blocking architectures—enabling incremental adoption without rewriting existing code
Minimal runtime dependencies: Built on Java standard library primitives like CompletableFuture<T> and Flow.Publisher<T> rather than heavyweight frameworks, ensuring clean integration into existing codebases and microservice architectures
Specification fidelity: Method signatures, documentation, and validation rules generated directly from OpenAPI definitions maintain accuracy between API contracts and client code, reducing integration surprises
// Initialize SDK with builder pattern - idiomatic Java designSDK sdk = SDK.builder() .serverURL("https://api.example.com") .apiKey("your-api-key") .build();// Type-safe method chaining with IDE autocompleteUser user = sdk.users() .userId("user-123") // Required field - compile-time safety .includeMetadata(true) // Optional field - null-friendly .call(); // Synchronous by default// Seamless async with same API - just add .async()CompletableFuture<User> asyncUser = sdk.async().users() .userId("user-123") .includeMetadata(true) .call();// Native reactive streams supportPublisher<Order> orderStream = sdk.async().orders() .status("active") .callAsPublisher();// Pagination with familiar Java patternssdk.orders() .status("completed") .callAsStream() // Returns java.util.Stream .filter(order -> order.amount() > 100) .limit(50) .forEach(System.out::println);// Rich exception handling with contexttry { User result = sdk.users().userId("invalid").call();} catch (APIException e) { // Detailed error context from OpenAPI spec System.err.println("API Error: " + e.getMessage()); System.err.println("Status: " + e.statusCode());}
Core Features
Type Safety & Null Handling
The SDK provides compile-time validation and runtime checks for required fields, with intuitive null handling:
Compile-time validation: Strong typing catches problems before runtime
Runtime validation: Required fields throw exceptions if missing
Null-friendly setters: Simple setters without Optional/JsonNullable wrapping
Smart getters: Return types match field semantics - direct access for required fields, Optional<T> for non-required fields, and JsonNullable<T> for non-required nullable fields
// Builder with various field typesUser user = User.builder() .id(123L) // Required primitive .name("John Doe") // Required string .email("john@example.com") // Required field .age(30) // Optional primitive - defaults if not set .bio("Developer") // Optional string - can be null .profileImage(null) // Nullable field - accepts null explicitly .build(); // Throws runtime exception if required fields missing// Type-safe getters with semantically appropriate return typesString name = user.name(); // Direct access for required fieldsOptional<Integer> age = user.age(); // Optional for non-required fieldsJsonNullable<String> bio = user.bio(); // JsonNullable for non-required + nullable fields// Method chaining with runtime validationCreateUserRequest request = CreateUserRequest.builder() .user(user) // Required - runtime exception if missing .sendWelcomeEmail(true) // Optional boolean .metadata(Map.of("source", "api")) // Optional complex type .build(); // Validates all required fields
Fluent Call-Builder Chaining
The SDK supports fluent method chaining that combines method builders with request builders for intuitive API calls:
For async iterables, the SDK leverages Reactive Streams Publisher<T> to provide:
Backpressure handling: Consumers control the rate of data processing
Ecosystem interoperability: Works with Project Reactor , RxJava, Akka Streams, and other reactive libraries
Resource efficiency: Memory-efficient processing of large datasets
Composition: Chain and transform async streams declaratively
The examples in this documentation use Flux from Project Reactor to demonstrate interoperability with reactive frameworks.
The SDK implements custom publishers, subscribers, and subscriptions using JDK-native operators while maintaining lightweight dependencies.
Async Pagination
For async pagination, use callAsPublisher() and callAsPublisherUnwrapped() methods that return reactive streams:
// Async pagination - returns Publisher<PageResponse>Publisher<UserPageResponse> userPages = sdk.async().listUsers() .callAsPublisher();// Async pagination unwrapped - returns Publisher<User> (concatenated items)Publisher<User> users = sdk.async().listUsers() .callAsPublisherUnwrapped();// Use with reactive libraries (Flux is from Project Reactor)Flux.from(users) .filter(User::isActive) .take(100) .subscribe(this::processUser);
Async Server-Sent Events
For async SSE, EventStream implements Publisher<EventType> directly:
// Async SSE streaming - EventStream implements Publisher and handles async responseEventStream<LogEvent> eventStream = sdk.async().streamLogs().events();// Process with reactive libraries - EventStream is a PublisherFlux.from(eventStream) .filter(event -> "ERROR".equals(event.getLevel())) .subscribe(this::handleErrorEvent);
Migration & DevX Improvements
Async-enabled SDKs provide backward compatibility, gradual adoption via .async() calls, and compatibility with Java 21+ virtual threads. Additional enhancements include null-friendly parameters, Jakarta annotations, enhanced error handling, and built-in timeout/cancellation support.
Package Structure
build.gradle
build-extras.gradle
gradlew
settings.gradle
...
Advanced Topics
Blob Abstraction
The Blob class provides efficient byte-stream handling across both sync and async methods:
Cat cat = Cat.builder().name("Whiskers").build();Dog dog = Dog.builder().name("Rex").build();// Pet.of accepts only Cat or Dog typesPet pet = Pet.of(cat);// Type inspection for handlingif (pet.value() instanceof Cat cat) { System.out.println("Cat: " + cat.name());} else if (pet.value() instanceof Dog dog) { System.out.println("Dog: " + dog.name());}
The anyOf keyword is treated as oneOf with forgiving deserialization—when multiple subtypes match, the heuristic selects the subtype with the greatest number of matching properties.
Enums
Closed Enums (standard Java enum):
public enum Color { RED("red"), GREEN("green"), BLUE("blue"); @JsonValue private final String value; public String value() { return value; } public static Optional<Color> fromValue(String value) { // Returns Optional.empty() for unknown values }}
Open Enums with x-speakeasy-unknown-values: allow:
// Looks like enum but handles unknown valuesColor red = Color.RED; // Static constantsColor unknown = Color.of("purple"); // Handles unknown valuesboolean isUnknown = unknown.isUnknown(); // Check if value is unknown// For switch expressions, convert to real enumunknown.asEnum().ifPresent(knownColor -> { switch (knownColor) { case RED -> System.out.println("Red"); // ... handle known values }});
Custom Serialization
You must use the generated custom Jackson ObjectMapper for serialization/deserialization:
// Access the singleton ObjectMapperObjectMapper mapper = JSON.getMapper();// Serialize/deserialize generated objectsString json = mapper.writeValueAsString(user);User user = mapper.readValue(json, User.class);
Build Customization
Preserve customizations: Use build-extras.gradle for additions (untouched by generation updates)
Add plugins: Use additionalPlugins property in gen.yaml
Manage dependencies: Add to build-extras.gradle or use additionalDependencies in gen.yaml
// Usage - passing Optional.empty() uses the OpenAPI defaultUser user = User.builder() .name("John") .status(Optional.empty()) // Will use "active" from OpenAPI spec .build();// Or omit the field entirely in buildersUser user = User.builder() .name("John") // status not specified - uses OpenAPI default "active" .build();
Important: Default values are lazy-loaded once. If the OpenAPI default is invalid for the field type (e.g., default: abc for type: integer), an IllegalArgumentException is thrown.
Workarounds for invalid defaults:
Regenerate SDK with corrected OpenAPI default
Always set the field explicitly to avoid lazy-loading the invalid default
Constant Values
Fields with const values are read-only and set internally:
// Const fields are not settable in constructors or buildersApiResponse response = ApiResponse.builder() .data(responseData) // version is automatically set to "1.0" - cannot be overridden .build();// But const values are readable via gettersString version = response.version(); // Returns "1.0"
Like default values, const values are lazy-loaded once. Invalid const values throw IllegalArgumentException.
User Agent Strings
The Java SDK includes a user agent string in all requests for tracking SDK usage: