Adding custom code

Fern-generated SDKs are designed to be extended with custom code. Your custom code can add additional functionality to the SDK and live in harmony with the generated code.

This page explains how to configure custom logic using a .fernignore file, create custom SDK methods, and add additional dependencies to your SDK.

Adding custom logic

If you want your SDK to do more than just make basic API calls (like combining multiple calls, processing data, adding utilities), you can use .fernignore to protect your custom code from being overwritten during regeneration.

Simply add your custom files to the SDK repository and list them out in .fernignore. Fern won’t override any files that you add in .fernignore.

To get started adding custom code:

1

Create a new file and add your custom logic

src/main/java/<package>/Helper.java
1package com.example.helper;
2
3public class Helper {
4
5public static void myHelper() {
6 System.out.println("Hello World!");
7}
8
9}
2

Add your file to .fernignore

A .fernignore file is automatically created in your SDK repository when you use GitHub publishing.
.fernignore
1# Specify files that shouldn't be modified by Fern
2
3src/main/java/<package>/Helper.java
3

Consume the helper

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

1import com.example.helper.Helper;
2
3public class Main {
4
5public static void main(String[] args) {
6 Helper.myHelper();
7}
8
9}

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.

1

Update generators.yml configuration

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

generators.yml
1- name: fernapi/fern-java-sdk
2 version: "..."
3 config:
4 client-class-name: BaseClient
2

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
1package com.example;
2
3import com.example.client.BaseClient;
4
5public class MyClient extends BaseClient { // extend the Fern generated client
6
7 public void myHelper() {
8 System.out.println("Hello World!");
9 }
10
11}
3

Update .fernignore

Add the MyClient.java to .fernignore.

.fernignore
1+ src/main/java/com/example/MyClient.java
4

Consume the method

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

1client.myHelper();

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.

Common use cases include:

  • Dynamic URL construction: Replace placeholders with runtime values (e.g., https://api.${DEV_NAMESPACE}.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:

1public class BaseApiClientBuilder {
2 protected ClientOptions buildClientOptions() {
3 ClientOptions.Builder builder = ClientOptions.builder();
4 setEnvironment(builder);
5 setAuthentication(builder); // Only if API has auth
6 setCustomHeaders(builder); // Only if API defines headers
7 setVariables(builder); // Only if API has variables
8 setHttpClient(builder);
9 setTimeouts(builder);
10 setRetries(builder);
11 setAdditional(builder);
12 return builder.build();
13 }
14}
1

Create a custom client class

Extend the generated base client:

src/main/java/com/example/MyClient.java
1package com.example;
2
3import com.example.api.BaseClient;
4import com.example.api.core.ClientOptions;
5
6public class MyClient extends BaseClient {
7 public MyClient(ClientOptions clientOptions) {
8 super(clientOptions);
9 }
10
11 public static MyClientBuilder builder() {
12 return new MyClientBuilder();
13 }
14}
2

Create a custom builder class

Override methods to customize behavior:

src/main/java/com/example/MyClientBuilder.java
1package com.example;
2
3import com.example.api.BaseClient.BaseClientBuilder;
4import com.example.api.core.ClientOptions;
5import com.example.api.core.Environment;
6
7public class MyClientBuilder extends BaseClientBuilder {
8
9 @Override
10 protected void setEnvironment(ClientOptions.Builder builder) {
11 String url = this.environment.getUrl();
12 String expandedUrl = expandEnvironmentVariables(url);
13 builder.environment(Environment.custom(expandedUrl));
14 }
15
16 @Override
17 protected void setAdditional(ClientOptions.Builder builder) {
18 builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
19 }
20
21 @Override
22 public MyClient build() {
23 return new MyClient(buildClientOptions());
24 }
25}
3

Update .fernignore

By adding these two files to .fernignore, fern will not update them on new generations.

.fernignore
1+ src/main/java/com/example/MyClient.java
2+ src/main/java/com/example/MyClientBuilder.java

Method reference

Each method serves a specific purpose and is only generated when needed:

MethodPurposeAvailable When
setEnvironment(builder)Customize environment/URL configurationAlways
setAuthentication(builder)Modify or add authenticationOnly if API has auth
setCustomHeaders(builder)Add custom headers defined in API specOnly if API defines headers
setVariables(builder)Configure API variablesOnly if API has variables
setHttpClient(builder)Customize OkHttp clientAlways
setTimeouts(builder)Modify timeout settingsAlways
setRetries(builder)Modify retry settingsAlways
setAdditional(builder)Final extension point for any custom configurationAlways
validateConfiguration()Add custom validation logicAlways
buildClientOptions()Orchestrates all configuration methods (rarely need to override)Always

Common patterns

1@Override
2protected void setEnvironment(ClientOptions.Builder builder) {
3 String baseUrl = this.environment.getUrl();
4 String tenantUrl = baseUrl.replace("/api/", "/tenants/" + tenantId + "/");
5 builder.environment(Environment.custom(tenantUrl));
6}
1@Override
2protected void setAuthentication(ClientOptions.Builder builder) {
3 builder.addHeader("Authorization", () ->
4 "Bearer " + tokenProvider.getAccessToken()
5 );
6}
1@Override
2protected void setAdditional(ClientOptions.Builder builder) {
3 // Add request tracking
4 builder.addHeader("X-Request-ID", () -> UUID.randomUUID().toString());
5 builder.addHeader("X-Client-Version", "1.0.0");
6
7 // Add feature flags
8 if (FeatureFlags.isEnabled("new-algorithm")) {
9 builder.addHeader("X-Feature-Flag", "new-algorithm");
10 }
11}
1@Override
2protected void setHttpClient(ClientOptions.Builder builder) {
3 OkHttpClient customClient = new OkHttpClient.Builder()
4 .connectTimeout(30, TimeUnit.SECONDS)
5 .readTimeout(60, TimeUnit.SECONDS)
6 .addInterceptor(new RetryInterceptor())
7 .connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
8 .build();
9 builder.httpClient(customClient);
10}

Requirements

  • Fern Java SDK version: 2.39.1 or later

Adding custom dependencies

Pro Feature

This feature is only available on paid plans. Please schedule a demo or email us to get started.

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

generators.yml
1- name: fernapi/fern-java-sdk
2 version: "..."
3 config:
4 custom-dependencies:
5 - org.apache.commons:commons-lang3:3.12.0
6 - org.slf4j:slf4j-api:2.0.7