@@ -104,106 +104,111 @@ To get started adding custom code:
104104
105105## Adding custom client configuration
106106
107- 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 .
107+ 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 .
108108
109109Common use cases include:
110- - ** Dynamic URL construction** : Replace placeholders with runtime values (e.g., ` https://api.${DEV_NAMESPACE }.example.com ` )
110+ - ** Dynamic URL construction** : Replace placeholders with runtime values (e.g., ` https://api.${TENANT }.example.com ` )
111111- ** Custom authentication** : Implement complex auth flows beyond basic token authentication
112112- ** Request transformation** : Add custom headers or modify requests globally
113-
114- ### How it works
115-
116- Generated builders use protected methods that can be overridden:
117-
118- ``` java
119- public class BaseApiClientBuilder {
120- protected ClientOptions buildClientOptions () {
121- ClientOptions . Builder builder = ClientOptions . builder();
122- setEnvironment(builder);
123- setAuthentication(builder); // Only if API has auth
124- setCustomHeaders(builder); // Only if API defines headers
125- setVariables(builder); // Only if API has variables
126- setHttpClient(builder);
127- setTimeouts(builder);
128- setRetries(builder);
129- setAdditional(builder);
130- return builder. build();
131- }
132- }
133- ```
113+ - ** Multi-tenant support** : Add tenant-specific configuration and headers
134114
135115<Steps >
136116
137- ### Create a custom client class
117+ ### Enable extensible builders
138118
139- Extend the generated base client :
119+ Add the flag to your ` generators.yml ` :
140120
141- ``` java title="src/main/java/com/example/MyClient.java"
142- package com.example ;
121+ ``` yaml {7} title="generators.yml"
122+ groups :
123+ local :
124+ generators :
125+ - name : fernapi/fern-java-sdk
126+ version : 2.39.6
127+ config :
128+ enable-extensible-builders : true
129+ ` ` `
143130
144- import com.example.api.BaseClient ;
145- import com.example.api.core.ClientOptions ;
131+ ### How it works
146132
147- public class MyClient extends BaseClient {
148- public MyClient (ClientOptions clientOptions ) {
149- super (clientOptions);
150- }
133+ Generated builders use the self-type pattern for type-safe method chaining:
151134
152- public static MyClientBuilder builder () {
153- return new MyClientBuilder ();
135+ ` ` ` java
136+ public abstract class BaseClientBuilder<T extends BaseClientBuilder<T>> {
137+ protected abstract T self();
138+
139+ public T token(String token) {
140+ return self(); // Returns your custom type, not BaseClientBuilder
154141 }
155142}
156143```
157144
158- ### Create a custom builder class
145+ ### Create a custom builder
159146
160- Override methods to customize behavior :
147+ Extend the generated builder :
161148
162- ``` java title="src/main/java/com/example/MyClientBuilder.java"
163- package com.example ;
164-
165- import com.example.api.BaseClient.BaseClientBuilder ;
166- import com.example.api.core.ClientOptions ;
167- import com.example.api.core.Environment ;
168-
169- public class MyClientBuilder extends BaseClientBuilder {
149+ ``` java title="src/main/java/com/example/CustomApiBuilder.java"
150+ public class CustomApiBuilder extends BaseClientBuilder<CustomApiBuilder > {
151+ @Override
152+ protected CustomApiBuilder self () {
153+ return this ;
154+ }
170155
171156 @Override
172157 protected void setEnvironment (ClientOptions .Builder builder ) {
158+ // Customize environment URL
173159 String url = this . environment. getUrl();
174160 String expandedUrl = expandEnvironmentVariables(url);
175161 builder. environment(Environment . custom(expandedUrl));
176162 }
177163
178164 @Override
179165 protected void setAdditional (ClientOptions .Builder builder ) {
166+ // Add custom headers
180167 builder. addHeader(" X-Request-ID" , () - > UUID . randomUUID(). toString());
181168 }
182-
183- @Override
184- public MyClient build () {
185- return new MyClient (buildClientOptions());
186- }
187169}
188170```
189171
172+ ### Use your custom builder
173+
174+ ``` java
175+ BaseClient client = new CustomApiBuilder ()
176+ .token(" my-token" ) // returns CustomApiBuilder
177+ .tenantId(" tenant-123" ) // returns CustomApiBuilder
178+ .timeout(30 ) // returns CustomApiBuilder
179+ .build();
180+
181+ client. users(). list();
182+ ```
183+
190184### Update ` .fernignore `
191185
192- By adding these two files to ` .fernignore ` , fern will not update them on new generations.
186+ Add your custom builder to ` .fernignore ` so Fern won't overwrite it:
193187
194188``` diff title=".fernignore"
195- + src/main/java/com/example/MyClient.java
196- + src/main/java/com/example/MyClientBuilder.java
189+ + src/main/java/com/example/CustomApiBuilder.java
197190```
198191
199192</Steps >
200193
194+ ### Default implementation
195+
196+ If you don't need to extend the builder, use the provided ` Impl ` class:
197+
198+ ``` java
199+ BaseClient client = BaseClientBuilder . Impl()
200+ .token(" my-token" )
201+ .timeout(30 )
202+ .build();
203+ ```
204+
201205### Method reference
202206
203207Each method serves a specific purpose and is only generated when needed:
204208
205209| Method | Purpose | Available When |
206210| --------| ---------| ----------------|
211+ | ` self() ` | Returns the concrete builder type for chaining | Always (abstract) |
207212| ` setEnvironment(builder) ` | Customize environment/URL configuration | Always |
208213| ` setAuthentication(builder) ` | Modify or add authentication | Only if API has auth |
209214| ` setCustomHeaders(builder) ` | Add custom headers defined in API spec | Only if API defines headers |
@@ -213,17 +218,16 @@ Each method serves a specific purpose and is only generated when needed:
213218| ` setRetries(builder) ` | Modify retry settings | Always |
214219| ` setAdditional(builder) ` | Final extension point for any custom configuration | Always |
215220| ` validateConfiguration() ` | Add custom validation logic | Always |
216- | ` buildClientOptions() ` | Orchestrates all configuration methods (rarely need to override) | Always |
217221
218222### Common patterns
219223
220224<Accordion title = " Multi-tenant URLs" >
221225``` java
222226@Override
223227protected void setEnvironment(ClientOptions . Builder builder) {
224- String baseUrl = this . environment. getUrl();
225- String tenantUrl = baseUrl . replace(" /api/" , " /tenants/" + tenantId + " /" );
226- builder. environment(Environment . custom(tenantUrl ));
228+ String url = this . environment. getUrl()
229+ .replace(" /api/" , " /tenants/" + tenantId + " /" );
230+ builder. environment(Environment . custom(url ));
227231}
228232```
229233</Accordion >
@@ -232,47 +236,54 @@ protected void setEnvironment(ClientOptions.Builder builder) {
232236``` java
233237@Override
234238protected void setAuthentication(ClientOptions . Builder builder) {
235- builder. addHeader(" Authorization" , () - >
239+ super . setAuthentication(builder); // Keep existing auth
240+ builder. addHeader(" Authorization" , () - >
236241 " Bearer " + tokenProvider. getAccessToken()
237242 );
238243}
239244```
240245</Accordion >
241246
242- <Accordion title = " Request tracking and monitoring " >
247+ <Accordion title = " Environment variable expansion " >
243248``` java
244249@Override
245- protected void setAdditional(ClientOptions . Builder builder) {
246- // Add request tracking
247- builder. addHeader(" X-Request-ID" , () - > UUID . randomUUID(). toString());
248- builder. addHeader(" X-Client-Version" , " 1.0.0" );
250+ protected void setEnvironment(ClientOptions . Builder builder) {
251+ String url = this . environment. getUrl();
252+ // Replace ${VAR_NAME} with environment variables
253+ Pattern pattern = Pattern . compile(" \\ $\\ {([^}]+)\\ }" );
254+ Matcher matcher = pattern. matcher(url);
255+ StringBuffer result = new StringBuffer ();
249256
250- // Add feature flags
251- if (FeatureFlags . isEnabled(" new-algorithm" )) {
252- builder. addHeader(" X-Feature-Flag" , " new-algorithm" );
257+ while (matcher. find()) {
258+ String envVar = System . getenv(matcher. group(1 ));
259+ matcher. appendReplacement(result,
260+ envVar != null ? envVar : matcher. group(0 ));
253261 }
262+ matcher. appendTail(result);
263+
264+ builder. environment(Environment . custom(result. toString()));
254265}
255266```
256267</Accordion >
257268
258- <Accordion title = " Advanced HTTP client configuration " >
269+ <Accordion title = " Request tracking " >
259270``` java
260271@Override
261- protected void setHttpClient(ClientOptions . Builder builder) {
262- OkHttpClient customClient = new OkHttpClient .Builder ()
263- .connectTimeout(30 , TimeUnit . SECONDS )
264- .readTimeout(60 , TimeUnit . SECONDS )
265- .addInterceptor(new RetryInterceptor ())
266- .connectionPool(new ConnectionPool (50 , 5 , TimeUnit . MINUTES ))
267- .build();
268- builder. httpClient(customClient);
272+ protected void setAdditional(ClientOptions . Builder builder) {
273+ builder. addHeader(" X-Request-ID" , () - > UUID . randomUUID(). toString());
274+ builder. addHeader(" X-Tenant-ID" , this . tenantId);
275+
276+ if (FeatureFlags . isEnabled(" new-feature" )) {
277+ builder. addHeader(" X-Feature-Flag" , " new-feature" );
278+ }
269279}
270280```
271281</Accordion >
272282
273283### Requirements
274284
275- - ** Fern Java SDK version** : 2.39.1 or later
285+ - ** Fern Java SDK version** : 2.39.6 or later
286+ - ** Configuration** : ` enable-extensible-builders: true ` in ` generators.yml `
276287
277288## Adding custom dependencies
278289
0 commit comments