Skip to content

feat(typescript,go,java): add clientDefault support to SDK generators#14541

Open
Swimburger wants to merge 32 commits intomainfrom
devin/1775171669-client-default-generators
Open

feat(typescript,go,java): add clientDefault support to SDK generators#14541
Swimburger wants to merge 32 commits intomainfrom
devin/1775171669-client-default-generators

Conversation

@Swimburger
Copy link
Copy Markdown
Member

@Swimburger Swimburger commented Apr 2, 2026

Description

Refs: Polytomic SDK upgrade — support x-fern-default client-side defaults in generated SDKs.

When a header, query parameter, or path parameter has a clientDefault value (populated via the x-fern-default OpenAPI extension, merged in #14355 and #14516), the generated SDK code now uses that value as a fallback when the user doesn't explicitly provide one.

Python support is in a separate PR (#14440).

Changes Made

TypeScript SDK Generator

  • New getClientDefaultValue() helper extracts string | boolean from the IR Literal union
  • Client options (global headers): options?.headerName ?? "clientDefault" instead of bare options?.headerName
  • Endpoint headers: appends ?? "clientDefault" fallback; for overridable root headers chains requestOptions?.header ?? this._options?.header ?? "clientDefault"
  • Endpoint headers (nullable guard): skips clientDefault fallback when the header type is nullable, so explicit null still means "don't send the header" instead of being silently replaced by the default
  • Query parameters: adds ?? "clientDefault" fallback on the scalar expression
  • Path parameters: adds ?? "clientDefault" fallback before URL interpolation
  • Request wrapper: parameters with clientDefault are made optional in the request interface (same as type-level defaults)
  • Snippets: uses client default value instead of YOUR_HEADER_NAME placeholder
  • Test files: added clientDefault: undefined to all HttpHeader object literals in 5 test files
  • IR SDK: bumped from 66.0.0-alpha.1 to 66.0.0 across all TS generator packages (32 files)

Go IR SDK (generators/go/internal/fern/ir/http.go)

  • Added ClientDefault *Literal field (bitmask bit 6) to HttpHeader, PathParameter, and QueryParameter structs with getters/setters

Go SDK Generator (generators/go/internal/generator/sdk.go)

  • Global headers in ToHeader(): starts with default value, env var overrides, then user-provided value overrides
  • Client constructor: applies clientDefault fallback after env var initialization; guarded by isStringType() to skip non-string headers
  • continue removal: the continue after header.Env in the constructor loop was removed so headers with both env and clientDefault fall through correctly
  • Endpoint headers: uses clientDefault as fallback when request field is zero-value
  • Query parameters: adds clientDefault via map-indexing guard (Go <1.17 compatible)
  • Path parameters: new pathParameterDefault struct; uses local variable to avoid mutating caller's wrapped request struct; isStringType() guard skips non-string path params
  • Renamed isStringPathParameterisStringType: now shared between path param and header guards

Java SDK Generator

  • IR SDK: bumped from irV65:65.4.0 to irV65:65.7.0 (v65-compatible types with clientDefault field). Note: irV65:66.0.0 was tried first but has v66 name-compressed types (NameOrString instead of Name) that break the existing generator — avoid that version.
  • versions.lock: constraint hash updated after irV65 upgrade (required by :verifyLocks Gradle task)
  • DefaultValueExtractor: new hasClientDefault(), extractClientDefault(), hasAnyDefault(), and extractEffectiveDefault() methods. clientDefault takes precedence over type-level defaults from PrimitiveTypeV2.
  • WrappedRequestGenerator: parameters with clientDefault become Optional in the wrapped request (same as type-level defaults), using hasAnyDefault() to check both. Added isAlreadyOptional() guard to prevent double-wrapping types that are already Optional<T>.
  • WrappedRequestEndpointWriter: headers with clientDefault use .orElse(defaultValue) instead of conditional if (isPresent()) blocks. New findHeaderClientDefault() looks up the IR HttpHeader by wire value across endpoint + service headers (endpoint first so findFirst() prefers endpoint overrides).
  • HttpUrlBuilder: query params use extractEffectiveDefault() (clientDefault > type-level). Path params with clientDefault use .orElse(defaultValue) for URL interpolation. New findQueryParamClientDefault() matches query params by wire key.

Seed / Config

  • Go seed.yml: remains at irVersion: v61 (Go IR SDK doesn't yet support v66 name compression)
  • Seed snapshots updated for x-fern-default fixture (ts-sdk: 4 files, go-sdk: 1 file — note: Go snapshot was generated at v66 locally but is not in CI fixture list)

Changelog Entries

  • TypeScript SDK versions.yml: v3.64.0 (feat: clientDefault support, irVersion 66)
  • Go SDK versions.yml: v1.34.0 (feat: clientDefault support, irVersion 61)
  • Java SDK versions.yml: v4.2.0 (feat: clientDefault support, irVersion 65)

Human Review Checklist

Java

  • IR SDK 65.7.0 compatibility: Verify irV65:65.7.0 is fully compatible with the existing Java generator. It was manually published and uses v65-compatible type shapes (e.g., NameAndWireValue not NameOrString). irV65:66.0.0 under the same artifact caused pervasive compile failures due to v66 name compression — do not use that version.
  • isAlreadyOptional guard: New static helper prevents double-wrapping already-optional types when clientDefault is set. Applied at all three wrapping sites (query params, path params, headers). Verify the isContainer() && getContainer().get().isOptional() check covers nullable containers if those can appear here.
  • findHeaderClientDefault type chain: Uses .flatMap(literal -> DefaultValueExtractor.extractClientDefault(Optional.of(literal))) because extractClientDefault accepts Optional<Literal> but is called after flatMap already unwrapped the Optional. Works correctly but is slightly awkward — consider adding an overload that accepts Literal directly.
  • WrappedRequestEndpointWriter stores httpService: A new httpService field was added to access service-level headers in findHeaderClientDefault(). Verify this is always non-null when the writer is constructed.
  • clientDefault precedence over type-level defaults: extractEffectiveDefault() prefers clientDefault over PrimitiveTypeV2 defaults. Verify this is the desired precedence.

Go

  • Go IR SDK manual edits (http.go): ClientDefault fields and bitmask bit 1 << 6 were hand-added. These will be overwritten on next IR SDK regeneration — verify the regenerated output matches.
  • Go irVersion remains v61: The Go IR SDK doesn't support v66 name compression, so seed.yml and versions.yml both use irVersion 61. The clientDefault feature code is present but cannot be exercised by CI seed tests until the Go IR SDK is migrated to v66. The x-fern-default Go snapshot directory exists but is not in the CI fixture list.
  • Go continue removal (sdk.go ~L1333): The continue after the header.Env block was removed so that headers with both env and clientDefault fall through correctly. Verify no regression for headers with env but no clientDefault.
  • Go constructor header isStringType guard: isStringType(header.ValueType) was added to guard the constructor's clientDefault block against non-string headers. Note: isStringType calls maybePrimitive, which recurses through optionals — so it returns true for Optional<String>. This is safe in practice (constructor headers are always plain strings), but the guard is technically permissive.
  • Go query param map-indexing check (sdk.go ~L1451): Verify that user-provided query params are already in queryParams by the time the clientDefault fallback runs (user params may be serialized via a different mechanism).
  • Go path param default assumes string (== ""): The isStringType() guard skips non-string path params, but verify the check covers all relevant cases (e.g., aliased strings, named strings).
  • Go path param local variable avoids mutation: The pathParameterDefault struct introduces a local var (e.g., _region) to prevent mutating the caller's wrapped request struct. Verify the variable naming doesn't collide with other generated variables.
  • Go constructor clientDefault block indentation (~L1334): The clientDefault block in WriteClient has inconsistent indentation relative to the surrounding header.Env block. Cosmetic only — does not affect behavior.

TypeScript

  • Boolean clientDefault: Boolean defaults call .toString() to produce "true"/"false" for header values. Verify this is the desired wire format.
  • Nullable header guard: When a header type is nullable AND has clientDefault, the clientDefault is skipped entirely. This is defensive but means nullable headers with clientDefault will never use the default. Verify this trade-off is acceptable.

Testing

  • No new unit tests added — relies on CI seed/snapshot tests
  • Java compiles successfully against irV65:65.7.0 (verified locally with ./gradlew compileJava)
  • Java Spotless formatting applied and passing
  • Java :verifyLocks passing after versions.lock constraint hash update
  • Manual code review of generated output patterns
  • TypeScript test files updated to include clientDefault: undefined on all HttpHeader object literals (5 files)
  • Seed snapshots updated for x-fern-default fixture (ts-sdk, go-sdk)
  • Go clientDefault code is present but not testable via CI until Go IR SDK supports v66
  • CI fully green (141/141 checks passing)
  • Biome formatting check passing
  • Lint check passing

Link to Devin session: https://app.devin.ai/sessions/dae61f87717a46d781e579e47b4758e5
Requested by: @Swimburger


Open with Devin

Swimburger and others added 2 commits April 2, 2026 23:26
…th params

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…th params

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

devin-ai-integration[bot]

This comment was marked as resolved.

…olean literals with fmt.Sprintf

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 2 commits April 3, 2026 00:49
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…path params

- Add utility methods to DefaultValueExtractor for clientDefault handling:
  hasClientDefault(), extractClientDefault(), extractEffectiveDefault(), hasAnyDefault()
- Update WrappedRequestGenerator to use hasAnyDefault() for making params optional
- Update HttpUrlBuilder to use extractEffectiveDefault() for query params and
  clientDefault fallback for path params via .orElse()
- Update WrappedRequestEndpointWriter to handle headers with clientDefault via .orElse()
- Bump Java IR SDK from 65.4.0 to 66.0.0 (includes clientDefault fields)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat: add clientDefault support to TypeScript and Go SDK generators feat: add clientDefault support to TypeScript, Go, and Java SDK generators Apr 3, 2026
Swimburger and others added 2 commits April 3, 2026 01:03
Add clientDefault: undefined to createPathParameter, createQueryParameter,
and createHttpHeader factory functions in test-utils/ir-factories.ts to
match IR SDK v66.0.0 type requirements.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add isStringPathParameter() type guard so clientDefault fallback code
(== "" check and fmt.Sprintf assignment) is only generated for
string-typed path parameters. Non-string types would cause compile errors.
Also fix indentation on path param and query param loops.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 3 commits April 3, 2026 01:23
For wrapped request path params with clientDefault, use a local variable
(e.g., _region := request.Region) instead of modifying request.Region
directly. This prevents silently mutating the caller's pointer-type
request struct when applying clientDefault fallbacks.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…versions.lock

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…pport

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat: add clientDefault support to TypeScript, Go, and Java SDK generators feat(typescript,go,java): add clientDefault support to SDK generators Apr 3, 2026
Swimburger and others added 4 commits April 3, 2026 01:40
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ed.yml irVersion, fix nullable header guard

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(typescript,go,java): add clientDefault support to SDK generators feat(typescript,go): add clientDefault support to SDK generators Apr 3, 2026
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(typescript,go): add clientDefault support to SDK generators feat(typescript,go,java): add clientDefault support to SDK generators Apr 3, 2026
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…o v61

- Java: Add isAlreadyOptional guard to prevent wrapping already-optional
  types in another Optional when clientDefault is set
- Go: Revert seed.yml irVersion to v61 (Go IR SDK doesn't support v66
  name compression yet)
- Go: Set versions.yml irVersion to 61 for consistency

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
}
if header.ClientDefault != nil {
f.P("if options.", header.Name.Name.PascalCase.UnsafeName, ` == "" {`)
f.P("options. ", header.Name.Name.PascalCase.UnsafeName, ` = fmt.Sprintf("%v", `, literalToValue(header.ClientDefault), `)`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Syntax error: extra space after "options." will generate invalid Go code like options. HeaderName = ... instead of options.HeaderName = ...

Fix:

f.P("options.", header.Name.Name.PascalCase.UnsafeName, ` = fmt.Sprintf("%v", `, literalToValue(header.ClientDefault), `)`)

This matches the pattern used on line 1312 for the env variable case.

Suggested change
f.P("options. ", header.Name.Name.PascalCase.UnsafeName, ` = fmt.Sprintf("%v", `, literalToValue(header.ClientDefault), `)`)
f.P("options.", header.Name.Name.PascalCase.UnsafeName, ` = fmt.Sprintf("%v", `, literalToValue(header.ClientDefault), `)`)

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

…tringPathParameter to isStringType

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 2 commits April 3, 2026 03:46
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
….1.1)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

🐛 1 issue in files not directly in the diff

🐛 TypeScript request wrapper does not make parameters optional when they have clientDefault (generators/typescript/sdk/request-wrapper-generator/src/GeneratedRequestWrapperImpl.ts:898-903)

The TypeScript request wrapper generator's hasDefaultValue method (GeneratedRequestWrapperImpl.ts:898-903) only checks PrimitiveTypeV2 type-level defaults (gated behind the useDefaultRequestParameterValues config flag). It does not check clientDefault. This means that when a query parameter, header, or path parameter has a clientDefault but its IR type is not already optional and has no PrimitiveTypeV2 default, the parameter remains required in the generated request wrapper interface. The ?? fallback added by the client-class-generator (e.g., limit ?? "100" at generators/typescript/sdk/client-class-generator/src/endpoints/utils/GeneratedQueryParams.ts:110-116, or region ?? "us-east-1" at generators/typescript/sdk/client-class-generator/src/endpoints/utils/buildUrl.ts:83-89) becomes dead code because the user must always supply a value.

By contrast, the Java generator explicitly wraps non-optional parameters in Optional when they have a clientDefault (generators/java/sdk/src/main/java/com/fern/java/client/generators/WrappedRequestGenerator.java:121-123), and the Go generator uses zero-value semantics. The TypeScript generator is missing this step, so clientDefault has no effect for parameters whose type is not already optional.

View 24 additional findings in Devin Review.

Open in Devin Review

…st wrapper

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

Fixed the TypeScript request wrapper bug identified by Devin Review (BUG-0001: parameters with clientDefault not marked optional).

Fix in bd74942: Added clientDefault != null checks at all three parameter sites (path params, query params, headers) in both getRequestProperties() and expensivelyComputeIfAllPropertiesAreOptional() in GeneratedRequestWrapperImpl.ts. Parameters with clientDefault are now marked optional in the generated request wrapper interface, so the ?? fallback in the client-class-generator is no longer dead code.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 23 additional findings in Devin Review.

Open in Devin Review

Comment on lines 1333 to +1338
}
if header.ClientDefault != nil && isStringType(header.ValueType) {
f.P("if options.", header.Name.Name.PascalCase.UnsafeName, ` == "" {`)
f.P("options. ", header.Name.Name.PascalCase.UnsafeName, ` = fmt.Sprintf("%v", `, literalToValue(header.ClientDefault), `)`)
f.P("}")
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Go generator: continue removed from header env-var loop changes behavior for headers without clientDefault

In generators/go/internal/generator/sdk.go:1328-1339, the continue statement was removed from the if header.Env != nil block in the client constructor's header initialization loop. The original code used continue to skip headers that already had an env-var fallback, preventing any further processing. While the new clientDefault block correctly handles the case where both Env and ClientDefault are present (env takes priority via the == "" guard), this structural change also means headers that have Env but no ClientDefault now fall through to the end of the loop body where there is no further processing — so behavior is unchanged for them.

However, the indentation of the clientDefault block (lines 1334-1338) is deeply inconsistent with the surrounding code, using extra tabs that make it appear nested inside the if header.Env != nil block when it is actually at the loop-body level. While Go ignores indentation, this makes the control flow misleading and fragile to future edits. A maintainer could easily misread the code as the clientDefault block being conditional on header.Env != nil, when it's actually unconditional.

Code showing misleading indentation
for _, header := range headers {
    if header.Env != nil {
        f.P("if options.", ..., ` == "" {`)
        f.P("options. ", ..., ` = os.Getenv(...)`)
        f.P("}")
    }
        if header.ClientDefault != nil && isStringType(header.ValueType) {  // looks nested but isn't
            f.P("if options.", ..., ` == "" {`)
            ...
        }
}
Suggested change
}
if header.ClientDefault != nil && isStringType(header.ValueType) {
f.P("if options.", header.Name.Name.PascalCase.UnsafeName, ` == "" {`)
f.P("options. ", header.Name.Name.PascalCase.UnsafeName, ` = fmt.Sprintf("%v", `, literalToValue(header.ClientDefault), `)`)
f.P("}")
}
if header.ClientDefault != nil && isStringType(header.ValueType) {
f.P("if options.", header.Name.Name.PascalCase.UnsafeName, ` == "" {`)
f.P("options. ", header.Name.Name.PascalCase.UnsafeName, ` = fmt.Sprintf("%v", `, literalToValue(header.ClientDefault), `)`)
f.P("}")
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good catch on the indentation — the clientDefault block is indeed at the loop-body level, not nested inside the if header.Env != nil block, but the extra tabs make it look nested. Will fix the indentation to align with the surrounding code.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 2 commits April 3, 2026 19:16
…es service

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… 3.64.0)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 2 commits April 3, 2026 19:46
…ompile errors

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…eConverter -> case rename)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 2 commits April 3, 2026 20:11
…aderClientDefault

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 2 commits April 3, 2026 20:42
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant