# Generate Java SDKs from OpenAPI / Swagger

## Overview

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
- **Built-in observability**: SLF4J integration provides structured logging across all SDK operations without framework lock-in, enabling comprehensive monitoring of HTTP requests, retries, streaming, and hook execution
- **Specification fidelity**: Method signatures, documentation, and validation rules generated directly from OpenAPI definitions maintain accuracy between API contracts and client code, reducing integration surprises

```java
// Initialize SDK with builder pattern - idiomatic Java design
SDK sdk = SDK.builder()
    .serverURL("https://api.example.com")
    .apiKey("your-api-key")
    .build();

// Type-safe method chaining with IDE autocomplete
User 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 support
Publisher<Order> orderStream = sdk.async().orders()
    .status("active")
    .callAsPublisher();

// Pagination with familiar Java patterns
sdk.orders()
    .status("completed")
    .callAsStream()                        // Returns java.util.Stream
    .filter(order -> order.amount() > 100)
    .limit(50)
    .forEach(System.out::println);

// Rich exception handling with context
try {
    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>`](https://github.com/OpenAPITools/jackson-databind-nullable) for non-required nullable fields

```java
// Builder with various field types
User 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 types
String name = user.name();                    // Direct access for required fields
Optional<Integer> age = user.age();           // Optional for non-required fields
JsonNullable<String> bio = user.bio();        // JsonNullable for non-required + nullable fields

// Method chaining with runtime validation
CreateUserRequest 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:

```java
// Fluent chaining: method builder → parameters → request body → call
User res = sdk.updateUser()
        .id("user-123")                                // Path/query parameters
        .payload(PatchUser.builder()
            .name("John Doe")                          // Request body fields
            .email("john@example.com")
            .build())
        .call();                                       // Execute request
```

### Authentication & Security

- OAuth flows and standard security mechanisms
- Custom enum types using string or integer values
- All-field constructors for compile-time OpenAPI change detection

## Synchronous Methods

### Basic Methods

Synchronous methods are the default mode for all SDK calls:

```java
// Standard synchronous calls
Portfolio portfolio = sdk.getPortfolio("user-123");
List<Trade> trades = sdk.getTrades(portfolio.getId());
```

### Pagination

For synchronous pagination, use `.call()`, `.callAsIterable()`, `.callAsStream()`, or `.callAsStreamUnwrapped()`:

- **`.call()`**: Returns the first page only
- **`.callAsIterable()`**: Returns `Iterable<>` for for-each iteration with automatic paging
- **`.callAsStream()`**: Returns `java.util.Stream` of pages with automatic paging
- **`.callAsStreamUnwrapped()`**: Returns `java.util.Stream` of concatenated items from all pages

```java
// Stream unwrapped example
sdk.searchDocuments()
   .contains("simple")
   .minSize(200)
   .maxSize(400)
   .callAsStreamUnwrapped()  // Returns Stream<Document>
   .filter(document -> "fiction".equals(document.category()))
   .limit(200)
   .forEach(System.out::println);
```

### Server-Sent Events

For synchronous SSE, use the `events()` method with try-with-resources:

```java
// Traverse event stream with while loop
try (EventStream<JsonEvent> events = response.events()) {
    Optional<JsonEvent> event;
    while ((event = events.next()).isPresent()) {
        processEvent(event.get());
    }
}

// Use with java.util.Stream
try (EventStream<JsonEvent> events = response.events()) {
    events.stream().forEach(this::processEvent);
}
```

### Error Handling

The SDK throws typed unchecked exceptions for all errors, organized in a hierarchy:

```
Base SDK Error
├── Default SDK Error (for network/IO errors and untyped API errors)
├── Default Async SDK Error (for async-specific errors)
└── Custom Errors (for typed error responses defined in OpenAPI spec)
```

Error class names can be customized via `gen.yaml` flags; if not specified, they're inferred from the SDK name.

#### Base SDK Error

All exceptions extend `RuntimeException` and encapsulate the raw HTTP response with accessors for:
- **Status code**: `statusCode()`
- **Headers**: `headers()`
- **Body**: `body()` returns `Optional<byte[]>` and `bodyAsString()` returns `Optional<String>` (accounts for cases where the body couldn't be read due to `IOException`)

#### Default SDK Error

The default SDK error is thrown during:
- **Network/IO errors**: Connection failures, timeouts, and other transport-level issues
- **Untyped API errors**: HTTP error responses without custom error schemas defined in the OpenAPI spec

#### Custom Error Responses

For operations with error responses defined in the OpenAPI spec, the SDK generates typed exception classes that encapsulate the error schema.

**Example OpenAPI spec:**
```yaml
paths:
  /users/{userId}:
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserError'
components:
  schemas:
    UserError:
      type: object
      properties:
        code:
          type: string
          enum: [NotFound, Unauthorized]
        reason:
          type: string
```

**Generated exception class:**
```java
public class UserError extends BaseSdkError {
    // Error schema as nested static class
    public static class Data {
        // Generated enum from OpenAPI spec
        public enum Code {
            NOT_FOUND("NotFound"),
            UNAUTHORIZED("Unauthorized");

            private final String value;
            Code(String value) { this.value = value; }
            public String value() { return value; }
        }

        private Code code;
        private String reason;
        // Getters and setters...
    }

    // Hoisted field accessors for convenience
    public Optional<Data.Code> code() { ... }
    public Optional<String> reason() { ... }

    // Full error object accessor
    public Optional<Data> data() { ... }

    // Available if deserialization failed
    public Optional<IOException> deserializationError() { ... }
}
```

All accessors return `Optional` to handle cases where the response body couldn't be deserialized.

**Usage:**
```java
try {
    User user = sdk.getUser("user-123");
} catch (UserError e) {
    // Handle typed error with field access
    e.code().ifPresent(code ->
        System.err.println("Error Code: " + code));
    e.reason().ifPresent(reason ->
        System.err.println("Reason: " + reason));

    // Check for deserialization issues
    if (e.deserializationError().isPresent()) {
        System.err.println("Failed to parse error response");
    }
} catch (SDKError e) {
    // Handle default SDK errors (network issues, untyped errors)
    System.err.println("Request failed: " + e.getMessage());
    System.err.println("Status: " + e.statusCode());
}
```

## Asynchronous Methods

### Dual SDK Architecture

Speakeasy Java SDKs implement a dual interface pattern that provides both synchronous and asynchronous programming models without breaking changes:

- **Synchronous by default**: All SDK instances work synchronously out of the box, maintaining compatibility with existing code.
- **Async opt-in**: Call `.async()` on any SDK instance to switch to asynchronous mode for that method chain.
- **Consistent API**: The same methods and parameters work in both modes, only the return types differ.

```java
// Single SDK instance serves both paradigms
TradingSDK sdk = TradingSDK.builder()
    .serverURL("https://api.example.com")
    .apiKey("your-api-key")
    .build();

// Synchronous usage
Portfolio portfolio = sdk.getPortfolio("user-123");
List<Trade> trades = sdk.getTrades(portfolio.getId());

// Asynchronous usage
CompletableFuture<Portfolio> asyncPortfolio = sdk.async()
    .getPortfolio("user-123");
CompletableFuture<List<Trade>> asyncTrades = asyncPortfolio
    .thenCompose(p -> 
        sdk.async().getTrades(p.getId()));
```

### Non-blocking I/O implementation

The async implementation uses Java 11's `HttpClient` async APIs and NIO.2 primitives for end-to-end non-blocking method calls:

- **HTTP requests**: `HttpClient.sendAsync()` with `CompletableFuture<HttpResponse<T>>`
- **File I/O**: `AsynchronousFileChannel` for non-blocking file methods
- **Stream processing**: `Flow.Publisher<List<ByteBuffer>>` for efficient byte handling

```java
// Underlying HTTP implementation
client.sendAsync(request, HttpResponse.BodyHandlers.ofPublisher())
    .thenApply(response -> response.body()) // Flow.Publisher<List<ByteBuffer>>
    .thenCompose(this::decodeJsonAsync);
```

### Reactive Streams integration

For async iterables, the SDK leverages [Reactive Streams](https://www.reactive-streams.org/) `Publisher<T>` to provide:

- **Backpressure handling**: Consumers control the rate of data processing
- **Ecosystem interoperability**: Works with [Project Reactor](https://projectreactor.io/), 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](https://projectreactor.io/) 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:

```java
// 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:

```java
// Async SSE streaming - EventStream implements Publisher and handles async response
EventStream<LogEvent> eventStream = sdk.async().streamLogs().events();

// Process with reactive libraries - EventStream is a Publisher
Flux.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

<FileTree>

  <FileTree.Folder name="src">
    <FileTree.Folder name="main">
      <FileTree.Folder name="{SDK Package Name}">

        <FileTree.Folder name="models">
          <FileTree.Folder name="operations">

          </FileTree.Folder>
          <FileTree.Folder name="shared">

          </FileTree.Folder>
          <FileTree.Folder name="errors">

          </FileTree.Folder>
        </FileTree.Folder>
        <FileTree.Folder name="operations">

        </FileTree.Folder>
        <FileTree.Folder name="utils">

          <FileTree.Folder name="pagination">

          </FileTree.Folder>
          <FileTree.Folder name="reactive">

          </FileTree.Folder>

        </FileTree.Folder>
        <FileTree.Folder name="hooks">

        </FileTree.Folder>
      </FileTree.Folder>
    </FileTree.Folder>
  </FileTree.Folder>
  <FileTree.Folder name="docs">

  </FileTree.Folder>

</FileTree>

## Advanced Topics

### Blob Abstraction

The `Blob` class provides efficient byte-stream handling across both sync and async methods:

```java
// Create from various sources
Blob.from(Paths.get("large-file.json"));    // File path
Blob.from(inputStream);                      // InputStream  
Blob.from("content");                        // String
Blob.from(publisherOfByteBuffers);          // Flow.Publisher

// Async consumption
CompletableFuture<byte[]> bytes = blob.toByteArray();
CompletableFuture<Path> path = blob.toFile(targetPath);
Publisher<ByteBuffer> stream = blob.asPublisher();
```

### HTTP Client Customization

The Java SDK HTTP client is configurable and supports both synchronous and asynchronous methods:

```java
public interface HTTPClient {
    HttpResponse<InputStream> send(HttpRequest request)
            throws IOException, InterruptedException, URISyntaxException;
      
    CompletableFuture<HttpResponse<Blob>> sendAsync(HttpRequest request);
}
```

A default implementation based on `java.net.HttpClient` provides sync and async patterns with connection pooling and streaming support.

### Custom Headers

Custom headers (those not explicitly defined in the OpenAPI spec) can be specified per-request using the call builder:
```java
CreatePaymentResponse res = sdk.payments().create()
    .paymentRequest(PaymentRequest.builder()
        .description("My first payment")
        .redirectUrl("https://example.org/redirect")
        .amount(10)
        .build())
    .header("IdempotencyKey", nextKey()) // custom header
    .call();
```
### Data Types & Serialization

The SDK uses native Java types where possible and provides custom handling for complex OpenAPI constructs.

#### Primitive and Native Types

Where possible, the Java SDK uses native types and primitives to increase null safety:

- `java.lang.String`
- `java.time.OffsetDateTime` for `date-time` format
- `java.time.LocalDate` for `date` format  
- `java.math.BigInteger` for unlimited-precision integers
- `java.math.BigDecimal` for unlimited-precision decimals
- `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`)

#### Unlimited-Precision Numerics

For applications requiring high-precision decimal or integer types (such as monetary amounts), use format specifications:

```yaml
# Unlimited-precision integer
type: integer
format: bigint
# OR
type: string
format: bigint
```

```yaml
# Unlimited-precision decimal  
type: number
format: decimal
# OR
type: string
format: decimal
```

Both map to `java.math.BigInteger` and `java.math.BigDecimal` respectively, with convenient builder overloads:

```java
// Object builders accept primitives directly
Payment.builder()
    .amount(99.99)              // Accepts double, converts to BigDecimal
    .transactionId(12345L)      // Accepts long, converts to BigInteger  
    .build();
```

#### Union Types

Support for polymorphic types uses OpenAPI's `oneOf` keyword with different strategies based on discriminator presence.

**Non-discriminated oneOf** uses composition:

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

```java
Cat cat = Cat.builder().name("Whiskers").build();
Dog dog = Dog.builder().name("Rex").build();

// Pet.of accepts only Cat or Dog types
Pet pet = Pet.of(cat);

// Type inspection for handling
if (pet.value() instanceof Cat cat) {
    System.out.println("Cat: " + cat.name());
} else if (pet.value() instanceof Dog dog) {
    System.out.println("Dog: " + dog.name());
}
```

**Discriminated oneOf** uses inheritance:

```yaml
Pet:
  oneOf:
    - $ref: "#/components/schemas/Cat"
    - $ref: "#/components/schemas/Dog"
  discriminator:
    propertyName: petType
```

```java
Pet cat = Cat.builder().name("Whiskers").build();  // Cat implements Pet
Pet dog = Dog.builder().name("Rex").build();       // Dog implements Pet
```

**oneOf Type Erasure Handling:**

When generic types would conflict, suffixed factory methods are generated:

```yaml
Info:
  oneOf:
    - type: array
      items: { type: integer }
      x-speakeasy-name-override: counts
    - type: array  
      items: { type: string }
      x-speakeasy-name-override: descriptions
```

```java
// Generates specialized factory methods to avoid erasure
Info countsInfo = Info.ofCounts(List.of(1, 2, 3));
Info descriptionsInfo = Info.ofDescriptions(List.of("a", "b", "c"));
```

**anyOf Support:**

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):

```java
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`:

```yaml
Color:
  type: string
  enum: [red, green, blue]
  x-speakeasy-unknown-values: allow
```

Generates a concrete class instead of Java enum:

```java
// Looks like enum but handles unknown values
Color red = Color.RED;                    // Static constants
Color unknown = Color.of("purple");       // Handles unknown values
boolean isUnknown = unknown.isUnknown();   // Check if value is unknown

// For switch expressions, convert to real enum
unknown.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:

```java
// Access the singleton ObjectMapper
ObjectMapper mapper = JSON.getMapper();

// Serialize/deserialize generated objects
String 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`

```yaml
java:
  version: 0.2.0
---
additionalPlugins:
  - 'id("java")'
additionalDependencies:
  - implementation:com.fasterxml.jackson.core:jackson-databind:2.16.0
```

### Logging & Observability

#### SLF4J Integration

The Java SDK includes built-in SLF4J logging integration that provides structured logging across all SDK operations without requiring specific logging implementations. This backend-agnostic approach allows library authors to include logging without depending on particular logging frameworks.

**Configuration:**

SLF4J logging is enabled by default for new SDKs via the `enableSlf4jLogging` configuration option. The feature can be configured in `gen.yaml`:

```yaml
java:
  version: 1.0.0
  enableSlf4jLogging: true  # Default: true for new SDKs
```

**Log Levels:**

- **DEBUG**: High-level operations (request/response cycles, retry attempts, hook executions)
- **TRACE**: Detailed information (pagination state, streaming lifecycle, backoff calculations)

**Logging Coverage:**

- **HTTP Client**: Request/response logging with sensitive header redaction
- **Retry Logic**: Attempt tracking, backoff calculations, and exhaustion reporting
- **Pagination**: Page fetch tracking and state management
- **Streaming**: Initialization, item processing, and lifecycle events
- **Hooks**: Execution counts, operation IDs, and exception handling

**Setup Example:**

Add a logging implementation dependency to your project:

```gradle
dependencies {
    implementation 'org.slf4j:slf4j-api:2.0.9'
    implementation 'ch.qos.logback:logback-classic:1.5.6'  // Example implementation
}
```

Create a `logback.xml` configuration file:

```xml
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- Set SDK logging to DEBUG level -->
    <logger name="com.example.sdk" level="DEBUG"/>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
```

**Backward Compatibility:**

Legacy `enableHTTPDebugLogging()` continues to work for existing SDKs. Both logging approaches can coexist during migration periods.

**Security:**

Sensitive headers (Authorization, X-API-Key, etc.) are automatically redacted in HTTP request/response logs to prevent credential leakage.

### Dependencies

The SDK maintains minimal dependencies:

- [Jackson Library](https://github.com/FasterXML/jackson) for JSON serialization/deserialization
- [Apache HttpClient](https://hc.apache.org/httpcomponents-client-4.5.x/index.html) for HTTP requests
- [Jayway JsonPath](https://github.com/json-path/JsonPath) for JSON path expressions in Speakeasy metadata
- [SLF4J API](https://www.slf4j.org/) for structured logging (when enabled)

## Configuration Reference

### Parameters & Method Generation

Method parameter handling is controlled by the `maxMethodParams` configuration in `gen.yaml`:

```yaml
java:
  version: 1.0.0
  maxMethodParams: 5  # Default threshold
```

**When parameter count ≤ `maxMethodParams`:**
```java
// Parameters become method arguments
User user = sdk.getUser("user-123", true, "email,name");
```

**When parameter count > `maxMethodParams` or `maxMethodParams = 0`:**
```java  
// All parameters wrapped in request object
GetUserRequest request = GetUserRequest.builder()
    .userId("user-123")
    .includeMetadata(true) 
    .fields("email,name")
    .build();
    
User user = sdk.getUser(request);
```

### Default Values & Constants

The SDK handles OpenAPI `default` and `const` keywords with lazy-loading behavior:

#### Default Values

Fields with `default` values use `Optional` wrappers and lazy-loading:

```yaml
# OpenAPI specification
User:
  type: object
  properties:
    status:
      type: string
      default: "active"
```

```java
// Usage - passing Optional.empty() uses the OpenAPI default
User user = User.builder()
    .name("John")
    .status(Optional.empty())  // Will use "active" from OpenAPI spec
    .build();

// Or omit the field entirely in builders
User 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:**
1. Regenerate SDK with corrected OpenAPI default
2. Always set the field explicitly to avoid lazy-loading the invalid default

#### Constant Values

Fields with `const` values are read-only and set internally:

```yaml
# OpenAPI specification
ApiResponse:
  type: object
  properties:
    version:
      type: string
      const: "1.0"
```

```java
// Const fields are not settable in constructors or builders
ApiResponse response = ApiResponse.builder()
    .data(responseData)
    // version is automatically set to "1.0" - cannot be overridden
    .build();

// But const values are readable via getters
String 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:

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

- `SDKVersion`: SDK version defined in `gen.yaml`
- `GenVersion`: Speakeasy generator version  
- `DocVersion`: OpenAPI document version
- `groupId.artifactId`: Concatenated from `gen.yaml` configuration
