> This page is for SDK, version Legacy.
> For other versions, use one of these documentation indexes:
> - Current (default): https://buildwithfern.com/learn/sdks/current/llms.txt
> - Legacy: https://buildwithfern.com/learn/sdks/legacy/llms.txt

> For clean Markdown content of this page, append .md to this URL. For the complete documentation index, see https://buildwithfern.com/learn/llms.txt.

# Adding custom code

> Augment your Java SDK with custom utilities

> **You're viewing the legacy SDK docs**
>
> These pages cover SDK generation configured with `generators.yml`. New projects use `sdk-config.yml`, so see [the Current version of this page](/learn/sdks/generators/java/custom-code) for up-to-date guidance. To move an existing project, follow the [SDK Config migration guide](/learn/sdks/legacy/generators/migration-guides/sdk-config), which walks through `fern sdk migrate`, and review the [`sdk-config.yml` reference](/learn/sdks/reference/sdk-config).

> **Enterprise feature**
>
> This feature is available only for the [Enterprise plan](https://buildwithfern.com/pricing). To get started, reach out to [support@buildwithfern.com](mailto:support@buildwithfern.com).

This page covers how to add custom logic, methods, interceptors, and dependencies to your Java SDK.

## Adding custom logic

To get started adding custom code:

> **Note**
>
> These steps cover adding a new custom file to the SDK. To preserve line-level edits to a generated file, use [Replay](/learn/sdks/legacy/overview/custom-code#replay) instead.

### Create a new file and add your custom logic

**`src/main/java/<package>/Helper.java`**

```java title="src/main/java/<package>/Helper.java"
package com.example.helper;

public class Helper {

public static void myHelper() {
    System.out.println("Hello World!");
}

}
```

### Add your file to `.fernignore`

**`.fernignore`**

```yaml {3} title=".fernignore"
# Specify files that shouldn't be modified by Fern

src/main/java/<package>/Helper.java
```

### Consume the helper

Now your users can consume the helper function by importing it from the SDK.

```java
import com.example.helper.Helper;

public class Main {

public static void main(String[] args) {
    Helper.myHelper();
}

}
```

## Adding custom SDK methods

Fern also allows you to add custom methods to the SDK itself (e.g.
`client.my_method()` ) by inheriting the Fern generated client and then
extending it.

### Update `generators.yml` configuration

Name your Fern-generated client something like `BaseClient` to reflect that this client will be extended.

**`generators.yml`**

```yml {4} title="generators.yml"
- name: fern-java-sdk
    version: "..."
    config:
      client-class-name: BaseClient
```

### Import and extend the generated client

First, import the Fern generated base client and extend it. Then, add whatever methods you want.

**`src/main/java/com/example/MyClient.java`**

```java title="src/main/java/com/example/MyClient.java"
package com.example;

import com.example.client.BaseClient;

public class MyClient extends BaseClient { // extend the Fern generated client

  public void myHelper() {
    System.out.println("Hello World!");
  }

}
```

### Update `.fernignore`

Add the `MyClient.java` to `.fernignore`.

**`.fernignore`**

```diff title=".fernignore"
+ src/main/java/com/example/MyClient.java
```

### Consume the method

Now your users can consume the helper function by importing it from the SDK.

```java
client.myHelper();
```

## Adding custom client configuration

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.${TENANT}.example.com`)
* **Custom authentication**: Implement complex auth flows beyond basic token authentication
* **Request transformation**: Add custom headers or modify requests globally
* **Multi-tenant support**: Add tenant-specific configuration and headers

### Enable extensible builders

Add the flag to your `generators.yml`:

**`generators.yml`**

```yaml {7} title="generators.yml"
groups:
  local:
    generators:
      - name: fern-java-sdk
        version: 2.39.6
        config:
          enable-extensible-builders: true
```

### How it works

Generated builders use the self-type pattern for type-safe method chaining:

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

Extend the generated builder:

**`src/main/java/com/example/CustomApiBuilder.java`**

```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());
    }
}
```

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

Add your custom builder to `.fernignore` so Fern won't overwrite it:

**`.fernignore`**

```diff title=".fernignore"
+ 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                                                                                      |
| `setVariables(builder)`      | Configure API variables                            | Only if API has variables                                                                                        |
| `setHttpClient(builder)`     | Customize OkHttp client                            | Always                                                                                                           |
| `setInterceptors(builder)`   | Attach custom OkHttp interceptors                  | Only if [`custom-interceptors`](/learn/sdks/legacy/generators/java/configuration#custom-interceptors) is enabled |
| `setTimeouts(builder)`       | Modify timeout settings                            | Always                                                                                                           |
| `setRetries(builder)`        | Modify retry settings                              | Always                                                                                                           |
| `setAdditional(builder)`     | Final extension point for any custom configuration | Always                                                                                                           |
| `validateConfiguration()`    | Add custom validation logic                        | Always                                                                                                           |

### Common patterns

#### Multi-tenant URLs

```java
@Override
protected void setEnvironment(ClientOptions.Builder builder) {
    String url = this.environment.getUrl()
        .replace("/api/", "/tenants/" + tenantId + "/");
    builder.environment(Environment.custom(url));
}
```

#### Dynamic authentication

```java
@Override
protected void setAuthentication(ClientOptions.Builder builder) {
    super.setAuthentication(builder); // Keep existing auth
    builder.addHeader("Authorization", () ->
        "Bearer " + tokenProvider.getAccessToken()
    );
}
```

#### Environment variable expansion

```java
@Override
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();
    
    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()));
}
```

#### Request tracking

```java
@Override
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.6 or later
* **Configuration**: `enable-extensible-builders: true` in `generators.yml`

## Adding custom interceptors

[OkHttp interceptors](https://square.github.io/okhttp/features/interceptors/) observe and rewrite every request and response an SDK sends, which covers request signing, public key client validation, and custom logging or tracing. Setting [`custom-interceptors`](/learn/sdks/legacy/generators/java/configuration#custom-interceptors) to `true` adds an `addInterceptor(okhttp3.Interceptor)` method to your generated client builder, giving the developers who install your SDK a supported extension point for those customizations:

**`generators.yml`**

```yaml {7} title="generators.yml"
groups:
  java-sdk:
    generators:
      - name: fern-java-sdk
        version: 4.32.4
        config:
          custom-interceptors: true
```

Developers using your SDK register interceptors as they construct the client. Each call to `addInterceptor` appends to the chain, and all are applied to the underlying `OkHttpClient` at build time:

```java
YourApiClient client = YourApiClient.builder()
    .token("<token>")
    .addInterceptor(chain -> chain.proceed(
        chain.request().newBuilder()
            .header("X-Request-Source", "checkout-service")
            .build()))
    .build();
```

Custom interceptors run in registration order, inside Fern's retry interceptor, so a retried request passes through them again and a signing interceptor signs each attempt. For OAuth APIs, `addInterceptor` is available both before and after the `token(...)` or `credentials(...)` call.

To attach an interceptor yourself rather than requiring every caller to register one, override the protected `setInterceptors(ClientOptions.Builder)` method on a [custom builder](#adding-custom-client-configuration), and add that builder to `.fernignore` so regeneration doesn't overwrite it.

## Closing the client

Generated root clients, both sync (`PlantsClient`) and async (`AsyncPlantsClient`), implement `AutoCloseable`. Calling `close()` shuts down the dispatcher executor of the underlying `OkHttpClient` and evicts its connection pool. Without this, the default `OkHttpClient`'s threads can keep a short-lived Java process (a CLI tool, batch job, or test) alive after the work is done.

Use `try-with-resources` to release resources when the block exits:

```java
try (PlantsClient client = PlantsClient.builder().token("<token>").build()) {
    client.plants().get("plant_123");
}
```

Or call `close()` explicitly:

```java
AsyncPlantsClient client = AsyncPlantsClient.builder().token("<token>").build();
client.plants().get("plant_123").join();
client.close();
```

`close()` only tears down an `OkHttpClient` that the SDK created. A client passed to the builder's `httpClient(...)` method is left running: the caller owns it and is responsible for closing it. Subpackage clients such as `client.plants()` aren't `AutoCloseable`; close the root client instead.

## Adding custom dependencies

To add packages that your custom code requires, update your `generators.yml`.

**`generators.yml`**

```yaml {4-7} title="generators.yml"
- name: fern-java-sdk
  version: "..."
  config:
    custom-dependencies:
      - org.apache.commons:commons-lang3:3.12.0
      - org.slf4j:slf4j-api:2.0.7

```