> 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 #### 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: These steps cover adding a new custom file to the SDK. To preserve line-level edits to a generated file, use [Replay](/learn/sdks/overview/custom-code#replay) instead. ### Create a new file and add your custom logic **`src/main/java//Helper.java`** ```java title="src/main/java//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//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> { 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 { @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/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/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.19.2 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("") .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. ## 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 ``` > Augment your Java SDK with custom utilities