Skip to content

feat(go): add clientDefault support to Go SDK generator#15215

Merged
Swimburger merged 19 commits intomainfrom
devin/1776787826-client-default-go
Apr 23, 2026
Merged

feat(go): add clientDefault support to Go SDK generator#15215
Swimburger merged 19 commits intomainfrom
devin/1776787826-client-default-go

Conversation

@Swimburger
Copy link
Copy Markdown
Member

@Swimburger Swimburger commented Apr 21, 2026

Description

Adds clientDefault support (from the x-fern-default OpenAPI extension) to the Go SDK generator. When a header, query parameter, or path parameter has a clientDefault value, the generated SDK uses that value as a fallback when the caller does not provide one.

Both the v1 and v2 Go generators are updated.

Changes Made

Go v1 generator (generators/go/internal/generator/sdk.go)

  • IR types (http.go): Added ClientDefault *Literal field to HttpHeader, QueryParameter, and PathParameter structs with getters, setters, and bitmask serialization control.
  • Request-options headers (sdk.go:WriteRequestOptionsDefinition): Headers with clientDefault initialize a local var with the default, optionally fall back to an env var, then use the caller-provided value if present.
  • Client constructor headers (sdk.go:WriteClient): After the existing env-var fallback, a second fallback to clientDefault is applied for string-typed headers. Note: the prior continue after the env block was removed so execution falls through to the new clientDefault check.
  • Endpoint headers: Endpoint-scoped headers with clientDefault use a local variable initialized with the default, overridden by the request field if provided.
  • Query parameters: After normal serialization, a clientDefault is added only if the key is not already present (if _, ok := queryParams[...]; !ok).
  • Path parameters: For both wrapped-request and positional styles, a local variable is initialized from the request field (or param), falling back to the clientDefault when the value is the zero string.
  • Helpers: Added isStringType() which guards clientDefault generation to non-optional string primitives, and pathParameterDefault struct for path param default tracking.

Go v2 generator (generators/go-v2/sdk/src/)

  • authUtils.ts: Added isPlainStringType() helper — returns true only for non-optional, non-nullable string primitives. Guards clientDefault generation for path params and headers since the == "" zero-value check only works for Go string (not *string).
  • ClientGenerator.ts: Modified writeHeaderEnvironmentVariables() to apply clientDefault fallback for global headers after the env-var fallback. Added writeClientDefaultConditional() method using safe getLiteralValue (AST-based, properly escaped).
  • HttpEndpointGenerator.ts:
    • Path parameters: Before URL encoding, applies clientDefault fallback using local variables (avoids mutating caller's request struct). Uses getFieldName() for safe Go identifiers. endpointURL := assignment moved after local var declarations.
    • Query parameters: After type-level defaults, adds clientDefault entries for query params that don't already have a type-level default. No type guard — works for both string and boolean literals (matches v1 behavior).
    • Endpoint headers: After existing header handling, applies clientDefault fallback using local variables (avoids request struct mutation).

Other

  • Unreleased changelog: Added generators/go/sdk/changes/unreleased/add-client-default-support.yml (uses the unreleased version mechanism instead of editing versions.yml directly).
  • Seed snapshots: Updated seed/go-sdk/x-fern-default/ (clientDefault output) and seed/go-sdk/exhaustive/ (unrelated UUID→Uuid naming regeneration from main).

Issues found during review & fixed

  • Security: unescaped string interpolation — Replaced all 3 getLiteralAsString callsites (which do no escaping) with safe getLiteralValue (AST-based go.TypeInstantiation.string()escapeGoString). Affected: ClientGenerator.ts:writeClientDefaultConditional, HttpEndpointGenerator.ts:buildEndpointUrl, HttpEndpointGenerator.ts:buildHeaders.
  • Request struct mutation — v2 endpoint headers and path parameters now use local variables (_FieldName := request.FieldName) instead of mutating the caller's *Request struct through the pointer.
  • Invalid Go syntax — Moved writer.write("endpointURL := ") from before clientDefault local var declarations to after them, preventing endpointURL := _region := request.Region (double :=).
  • Unsafe Go identifiers — Path param local variable names now use getFieldName() instead of getOriginalName() to handle names with hyphens/special characters.
  • Silently dropped boolean defaults — Removed isPlainStringType guard for query param clientDefault in v2 (query params use a defaults map, so any literal type works — matches v1 behavior).

Human Review Checklist

Items worth a reviewer's attention — not all are bugs, but they carry risk.

  • continue removal in v1 client constructor (~line 1330): The continue after the env-var block was removed so execution falls through to the clientDefault check. When env != nil but clientDefault == nil, the code enters the env block then skips the clientDefault block (guarded by clientDefault != nil). Behavior should be equivalent, but worth verifying.
  • Space in options. field access (v1 lines ~1334, 1336): f.P("options. ", ...) has a space — this is pre-existing in the codebase. Go's f.P concatenates args, so it generates options. HeaderName. Confirm this is intentional.
  • isPlainStringType / isStringType don't resolve aliases: If a type is an alias of string, clientDefault will be silently skipped. Same limitation in both v1 and v2.
  • Pre-existing getLiteralAsString calls for type-level literals: Lines 832 and 886 of HttpEndpointGenerator.ts still use getLiteralAsString for type-level literal values (not clientDefault). These have the same escaping gap but are pre-existing and out of scope for this PR.
  • _FieldName local variable prefix convention: Path param and header clientDefault logic creates local variables prefixed with _ (e.g., _Region). Verify this doesn't collide with any Go reserved identifiers or existing generated variable names.
  • Query param clientDefault extracts raw string from Literal: queryParameter.clientDefault.string is passed to go.TypeInstantiation.string() which handles escaping. Confirm this is safe for all string contents (quotes, backslashes, newlines).
  • Exhaustive snapshot churn: The seed/go-sdk/exhaustive/ changes are unrelated to clientDefault — they're from main's UUID→Uuid naming convention update and metadata cleanup that got regenerated when running seed tests.

Testing

  • v2 TypeScript compilation (tsc) passed via seed test build
  • Self-review of full diff completed
  • All Devin Review issues resolved (6 findings addressed across 6 commits)
  • hex-security-app finding addressed (unescaped getLiteralAsString → safe getLiteralValue)
  • Docker seed tests: x-fern-default fixture — generated Go code compiles (go build) and passes tests (go test) ✓
  • Docker seed tests: exhaustive fixture (both variants) — compiles and passes tests ✓
  • Seed snapshots committed (x-fern-default + exhaustive regeneration)
  • CI fully green: 53/53 checks passing (compile, biome, depcheck, lint, test, test-ete, all 16 go-sdk seed jobs)
  • Unit tests added/updated

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


Open in Devin Review

- Support x-fern-default fallback values for headers, query params, and path params
- Headers: use clientDefault as fallback when no value provided
- Query params: use clientDefault as fallback value
- Path params: use clientDefault as default parameter value
- Add ClientDefault field to HttpHeader, QueryParameter, and PathParameter IR types
- Update versions.yml with new feature entry

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

devin-ai-integration[bot]

This comment was marked as resolved.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

SDK Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-23T04:59:11Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
go-sdk square 129s 166s 119s -10s (-7.8%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-04-23T04:59:11Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-04-23 16:53 UTC

Swimburger and others added 2 commits April 21, 2026 16:48
Add clientDefault fallback for headers, path params, and query params
in the Go v2 generator (ClientGenerator.ts and HttpEndpointGenerator.ts).

- ClientGenerator: apply clientDefault after env fallback for global headers
- HttpEndpointGenerator: apply clientDefault for endpoint headers, path params, query params
- Add isPlainStringType helper to guard clientDefault (only non-optional string types)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Use go.TypeInstantiation.string() for map entry values (not getLiteralValue
which returns AstNode). Simplify dedup logic using a Set of wire values.

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

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Swimburger and others added 3 commits April 21, 2026 16:59
…er's request struct

For endpoint headers and path parameters, the clientDefault fallback was
directly writing to request struct fields (e.g., request.Region = default),
which mutates the caller's input through the pointer. Now uses local
variables instead, matching v1's pattern.

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>
…3 to 2026-04-21

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 21, 2026 17:14
…in v2

The writer.write('endpointURL := ') was emitted before the clientDefault
local variable declarations, producing invalid Go like:
  endpointURL := _region := request.Region

Fix: move 'endpointURL := ' to just before the callEncodeUrl call, so
local variable declarations are on their own lines before the assignment.
For endpoints with no path params, keep the original inline pattern.

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.

… in v2

Query parameters use a defaults map approach (callQueryValuesWithDefaults)
where values are always stringified, so the isPlainStringType restriction
was unnecessarily dropping boolean clientDefaults. This makes v2 consistent
with v1's behavior at sdk.go:1457 which has no type restriction.

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 8 commits April 21, 2026 21:18
…riable

getOriginalName returns the raw API name (e.g. 'my-region') which can
contain hyphens and other characters invalid in Go identifiers. Use
this.context.getFieldName() instead, matching how endpoint headers
handle the same pattern at line 912.

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

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

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>
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>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Comment thread generators/go/sdk/versions.yml Outdated
@@ -1,4 +1,16 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.36.0
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.

Can update this with the new unreleased mechanism.

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.

Thanks for the suggestion! I'm not familiar with the "unreleased mechanism" — I don't see it used in any other versions.yml files on main currently. Could you point me to an example or docs? Happy to update if there's a specific format to follow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Instead of editing versions.yml, use the unreleased version mechanism. It is documented in CLAUDE.md and other places.

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.

Done — reverted the versions.yml edit and created generators/go/sdk/changes/unreleased/add-client-default-support.yml instead (commit d584b11a925).

r.baseURL,
"",
)
_Region := request.Region
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.

Looks good

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.

Thanks! 👍

… edit

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@Swimburger Swimburger enabled auto-merge (squash) April 23, 2026 16:47
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 19 additional findings in Devin Review.

Open in Devin Review

Comment on lines +788 to +795
const literalString =
queryParameter.clientDefault.type === "string"
? queryParameter.clientDefault.string
: String(queryParameter.clientDefault.boolean);
defaults.push({
key: go.TypeInstantiation.string(wireValue),
value: go.TypeInstantiation.string(literalString)
});
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.

🟡 Boolean clientDefault for query parameter produces wrong Go type in defaults map

When a query parameter has a boolean clientDefault, the v2 code at lines 788-794 converts it to a Go string literal (e.g., "true") instead of a Go boolean literal (true). This is inconsistent with how type-level boolean defaults are stored — those use go.TypeInstantiation.bool(value). While the URL-encoded output is the same (both stringify to "true"), the actual bug manifests when QueryValuesWithDefaults (generators/go/internal/generator/sdk/internal/query.go:95) checks field.IsZero() on the struct field. For a boolean field set to false, IsZero() returns true, so the default map is consulted. The default should be a Go bool value (true/false), not a Go string value ("true"/"false"). A string default in a map[string]interface{} would be passed to valueString(reflect.ValueOf("true")), which produces the correct URL string, so the end result is functionally equivalent. However, the comment on line 782-783 claims "any literal type works here" yet always forces the value to a string type, making the comment misleading and the implementation inconsistent with the type-level defaults mechanism.

Suggested change
const literalString =
queryParameter.clientDefault.type === "string"
? queryParameter.clientDefault.string
: String(queryParameter.clientDefault.boolean);
defaults.push({
key: go.TypeInstantiation.string(wireValue),
value: go.TypeInstantiation.string(literalString)
});
const literalValue =
queryParameter.clientDefault.type === "string"
? go.TypeInstantiation.string(queryParameter.clientDefault.string)
: go.TypeInstantiation.bool(queryParameter.clientDefault.boolean);
defaults.push({
key: go.TypeInstantiation.string(wireValue),
value: literalValue
});
Open in Devin Review

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

@Swimburger Swimburger merged commit 822784f into main Apr 23, 2026
180 of 187 checks passed
@Swimburger Swimburger deleted the devin/1776787826-client-default-go branch April 23, 2026 17:53
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.

2 participants