diff --git a/fern/products/sdks/overview/java/custom-code.mdx b/fern/products/sdks/overview/java/custom-code.mdx
index 18e96978c..af0efd1d8 100644
--- a/fern/products/sdks/overview/java/custom-code.mdx
+++ b/fern/products/sdks/overview/java/custom-code.mdx
@@ -104,72 +104,58 @@ To get started adding custom code:
## Adding custom client configuration
-The Java SDK generator supports builder extensibility through a template method pattern. By extending the generated builder classes and overriding protected methods, you can customize how your SDK client is configured without modifying generated code.
+The Java SDK generator supports builder extensibility through an opt-in self-type pattern. When enabled via the `enable-extensible-builders` flag, generated builders can be extended while maintaining type safety during method chaining.
Common use cases include:
-- **Dynamic URL construction**: Replace placeholders with runtime values (e.g., `https://api.${DEV_NAMESPACE}.example.com`)
+- **Dynamic URL construction**: Replace placeholders with runtime values (e.g., `https://api.${TENANT}.example.com`)
- **Custom authentication**: Implement complex auth flows beyond basic token authentication
- **Request transformation**: Add custom headers or modify requests globally
-
-### How it works
-
-Generated builders use protected methods that can be overridden:
-
-```java
-public class BaseApiClientBuilder {
- protected ClientOptions buildClientOptions() {
- ClientOptions.Builder builder = ClientOptions.builder();
- setEnvironment(builder);
- setAuthentication(builder); // Only if API has auth
- setCustomHeaders(builder); // Only if API defines headers
- setVariables(builder); // Only if API has variables
- setHttpClient(builder);
- setTimeouts(builder);
- setRetries(builder);
- setAdditional(builder);
- return builder.build();
- }
-}
-```
+- **Multi-tenant support**: Add tenant-specific configuration and headers
-### Create a custom client class
+### Enable extensible builders
-Extend the generated base client:
+Add the flag to your `generators.yml`:
-```java title="src/main/java/com/example/MyClient.java"
-package com.example;
+```yaml {7} title="generators.yml"
+groups:
+ local:
+ generators:
+ - name: fernapi/fern-java-sdk
+ version: 2.39.6
+ config:
+ enable-extensible-builders: true
+```
-import com.example.api.BaseClient;
-import com.example.api.core.ClientOptions;
+### How it works
-public class MyClient extends BaseClient {
- public MyClient(ClientOptions clientOptions) {
- super(clientOptions);
- }
+Generated builders use the self-type pattern for type-safe method chaining:
- public static MyClientBuilder builder() {
- return new MyClientBuilder();
+```java
+public abstract class BaseClientBuilder> {
+ protected abstract T self();
+
+ public T token(String token) {
+ return self(); // Returns your custom type, not BaseClientBuilder
}
}
```
-### Create a custom builder class
+### Create a custom builder
-Override methods to customize behavior:
+Extend the generated builder:
-```java title="src/main/java/com/example/MyClientBuilder.java"
-package com.example;
-
-import com.example.api.BaseClient.BaseClientBuilder;
-import com.example.api.core.ClientOptions;
-import com.example.api.core.Environment;
-
-public class MyClientBuilder extends BaseClientBuilder {
+```java title="src/main/java/com/example/CustomApiBuilder.java"
+public class CustomApiBuilder extends BaseClientBuilder {
+ @Override
+ protected CustomApiBuilder self() {
+ return this;
+ }
@Override
protected void setEnvironment(ClientOptions.Builder builder) {
+ // Customize environment URL
String url = this.environment.getUrl();
String expandedUrl = expandEnvironmentVariables(url);
builder.environment(Environment.custom(expandedUrl));
@@ -177,33 +163,52 @@ public class MyClientBuilder extends BaseClientBuilder {
@Override
protected void setAdditional(ClientOptions.Builder builder) {
+ // Add custom headers
builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
}
-
- @Override
- public MyClient build() {
- return new MyClient(buildClientOptions());
- }
}
```
+### Use your custom builder
+
+```java
+BaseClient client = new CustomApiBuilder()
+ .token("my-token") // returns CustomApiBuilder
+ .tenantId("tenant-123") // returns CustomApiBuilder
+ .timeout(30) // returns CustomApiBuilder
+ .build();
+
+client.users().list();
+```
+
### Update `.fernignore`
-By adding these two files to `.fernignore`, fern will not update them on new generations.
+Add your custom builder to `.fernignore` so Fern won't overwrite it:
```diff title=".fernignore"
-+ src/main/java/com/example/MyClient.java
-+ src/main/java/com/example/MyClientBuilder.java
++ src/main/java/com/example/CustomApiBuilder.java
```
+### Default implementation
+
+If you don't need to extend the builder, use the provided `Impl` class:
+
+```java
+BaseClient client = BaseClientBuilder.Impl()
+ .token("my-token")
+ .timeout(30)
+ .build();
+```
+
### Method reference
Each method serves a specific purpose and is only generated when needed:
| Method | Purpose | Available When |
|--------|---------|----------------|
+| `self()` | Returns the concrete builder type for chaining | Always (abstract) |
| `setEnvironment(builder)` | Customize environment/URL configuration | Always |
| `setAuthentication(builder)` | Modify or add authentication | Only if API has auth |
| `setCustomHeaders(builder)` | Add custom headers defined in API spec | Only if API defines headers |
@@ -213,7 +218,6 @@ Each method serves a specific purpose and is only generated when needed:
| `setRetries(builder)` | Modify retry settings | Always |
| `setAdditional(builder)` | Final extension point for any custom configuration | Always |
| `validateConfiguration()` | Add custom validation logic | Always |
-| `buildClientOptions()` | Orchestrates all configuration methods (rarely need to override) | Always |
### Common patterns
@@ -221,9 +225,9 @@ Each method serves a specific purpose and is only generated when needed:
```java
@Override
protected void setEnvironment(ClientOptions.Builder builder) {
- String baseUrl = this.environment.getUrl();
- String tenantUrl = baseUrl.replace("/api/", "/tenants/" + tenantId + "/");
- builder.environment(Environment.custom(tenantUrl));
+ String url = this.environment.getUrl()
+ .replace("/api/", "/tenants/" + tenantId + "/");
+ builder.environment(Environment.custom(url));
}
```
@@ -232,47 +236,54 @@ protected void setEnvironment(ClientOptions.Builder builder) {
```java
@Override
protected void setAuthentication(ClientOptions.Builder builder) {
- builder.addHeader("Authorization", () ->
+ super.setAuthentication(builder); // Keep existing auth
+ builder.addHeader("Authorization", () ->
"Bearer " + tokenProvider.getAccessToken()
);
}
```
-
+
```java
@Override
-protected void setAdditional(ClientOptions.Builder builder) {
- // Add request tracking
- builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
- builder.addHeader("X-Client-Version", "1.0.0");
+protected void setEnvironment(ClientOptions.Builder builder) {
+ String url = this.environment.getUrl();
+ // Replace ${VAR_NAME} with environment variables
+ Pattern pattern = Pattern.compile("\\$\\{([^}]+)\\}");
+ Matcher matcher = pattern.matcher(url);
+ StringBuffer result = new StringBuffer();
- // Add feature flags
- if (FeatureFlags.isEnabled("new-algorithm")) {
- builder.addHeader("X-Feature-Flag", "new-algorithm");
+ while (matcher.find()) {
+ String envVar = System.getenv(matcher.group(1));
+ matcher.appendReplacement(result,
+ envVar != null ? envVar : matcher.group(0));
}
+ matcher.appendTail(result);
+
+ builder.environment(Environment.custom(result.toString()));
}
```
-
+
```java
@Override
-protected void setHttpClient(ClientOptions.Builder builder) {
- OkHttpClient customClient = new OkHttpClient.Builder()
- .connectTimeout(30, TimeUnit.SECONDS)
- .readTimeout(60, TimeUnit.SECONDS)
- .addInterceptor(new RetryInterceptor())
- .connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
- .build();
- builder.httpClient(customClient);
+protected void setAdditional(ClientOptions.Builder builder) {
+ builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
+ builder.addHeader("X-Tenant-ID", this.tenantId);
+
+ if (FeatureFlags.isEnabled("new-feature")) {
+ builder.addHeader("X-Feature-Flag", "new-feature");
+ }
}
```
### Requirements
-- **Fern Java SDK version**: 2.39.1 or later
+- **Fern Java SDK version**: 2.39.6 or later
+- **Configuration**: `enable-extensible-builders: true` in `generators.yml`
## Adding custom dependencies