Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 87 additions & 76 deletions fern/products/sdks/overview/java/custom-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -104,106 +104,111 @@ 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think most people reading this page would understand what "self-type pattern" means? I'd consider wording this differently if not. (Feel free to ignore this if you think it's clear the way it is, I'm no Java expert!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so! If the users are comfortable with Java they should know what this provides.


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

<Steps>

### 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<T extends BaseClientBuilder<T>> {
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<CustomApiBuilder> {
@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));
}

@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
```

</Steps>

### Default implementation

If you don't need to extend the builder, use the provided `Impl` class:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it'd be pretty clear to someone when they would or wouldn't need to extend the builder? If not, you could add another sentence describing a typical use case for someone who doesn't need to extend the builder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I think so actually! If they want base client options (most customers just rely on the one that is generated) it would make sense.


```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 |
Expand All @@ -213,17 +218,16 @@ 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

<Accordion title="Multi-tenant URLs">
```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));
}
```
</Accordion>
Expand All @@ -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()
);
}
```
</Accordion>

<Accordion title="Request tracking and monitoring">
<Accordion title="Environment variable expansion">
```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()));
}
```
</Accordion>

<Accordion title="Advanced HTTP client configuration">
<Accordion title="Request tracking">
```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");
}
}
```
</Accordion>

### 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`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be great if you could add a new entry with a brief description in the Java Configuration page for enable-extensible-builders!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!


## Adding custom dependencies

Expand Down