feat(go): add clientDefault support to Go SDK generator#15215
feat(go): add clientDefault support to Go SDK generator#15215Swimburger merged 19 commits intomainfrom
Conversation
- 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>
There was a problem hiding this comment.
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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
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 |
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>
…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>
…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>
… 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>
…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>
| @@ -1,4 +1,16 @@ | |||
| # yaml-language-server: $schema=../../../fern-versions-yml.schema.json | |||
| - version: 1.36.0 | |||
There was a problem hiding this comment.
Can update this with the new unreleased mechanism.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Instead of editing versions.yml, use the unreleased version mechanism. It is documented in CLAUDE.md and other places.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Thanks! 👍
… edit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| 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) | ||
| }); |
There was a problem hiding this comment.
🟡 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.
| 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 | |
| }); |
Was this helpful? React with 👍 or 👎 to provide feedback.
Description
Adds
clientDefaultsupport (from thex-fern-defaultOpenAPI extension) to the Go SDK generator. When a header, query parameter, or path parameter has aclientDefaultvalue, 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)http.go): AddedClientDefault *Literalfield toHttpHeader,QueryParameter, andPathParameterstructs with getters, setters, and bitmask serialization control.sdk.go:WriteRequestOptionsDefinition): Headers withclientDefaultinitialize a local var with the default, optionally fall back to an env var, then use the caller-provided value if present.sdk.go:WriteClient): After the existing env-var fallback, a second fallback toclientDefaultis applied for string-typed headers. Note: the priorcontinueafter the env block was removed so execution falls through to the new clientDefault check.clientDefaultuse a local variable initialized with the default, overridden by the request field if provided.clientDefaultis added only if the key is not already present (if _, ok := queryParams[...]; !ok).isStringType()which guards clientDefault generation to non-optional string primitives, andpathParameterDefaultstruct for path param default tracking.Go v2 generator (
generators/go-v2/sdk/src/)authUtils.ts: AddedisPlainStringType()helper — returns true only for non-optional, non-nullablestringprimitives. Guards clientDefault generation for path params and headers since the== ""zero-value check only works for Gostring(not*string).ClientGenerator.ts: ModifiedwriteHeaderEnvironmentVariables()to apply clientDefault fallback for global headers after the env-var fallback. AddedwriteClientDefaultConditional()method using safegetLiteralValue(AST-based, properly escaped).HttpEndpointGenerator.ts:getFieldName()for safe Go identifiers.endpointURL :=assignment moved after local var declarations.Other
generators/go/sdk/changes/unreleased/add-client-default-support.yml(uses the unreleased version mechanism instead of editingversions.ymldirectly).seed/go-sdk/x-fern-default/(clientDefault output) andseed/go-sdk/exhaustive/(unrelated UUID→Uuid naming regeneration from main).Issues found during review & fixed
getLiteralAsStringcallsites (which do no escaping) with safegetLiteralValue(AST-basedgo.TypeInstantiation.string()→escapeGoString). Affected:ClientGenerator.ts:writeClientDefaultConditional,HttpEndpointGenerator.ts:buildEndpointUrl,HttpEndpointGenerator.ts:buildHeaders._FieldName := request.FieldName) instead of mutating the caller's*Requeststruct through the pointer.writer.write("endpointURL := ")from before clientDefault local var declarations to after them, preventingendpointURL := _region := request.Region(double:=).getFieldName()instead ofgetOriginalName()to handle names with hyphens/special characters.isPlainStringTypeguard for query param clientDefault in v2 (query params use a defaults map, so any literal type works — matches v1 behavior).Human Review Checklist
continueremoval in v1 client constructor (~line 1330): Thecontinueafter the env-var block was removed so execution falls through to the clientDefault check. Whenenv != nilbutclientDefault == nil, the code enters the env block then skips the clientDefault block (guarded byclientDefault != nil). Behavior should be equivalent, but worth verifying.options.field access (v1 lines ~1334, 1336):f.P("options. ", ...)has a space — this is pre-existing in the codebase. Go'sf.Pconcatenates args, so it generatesoptions. HeaderName. Confirm this is intentional.isPlainStringType/isStringTypedon't resolve aliases: If a type is an alias ofstring, clientDefault will be silently skipped. Same limitation in both v1 and v2.getLiteralAsStringcalls for type-level literals: Lines 832 and 886 ofHttpEndpointGenerator.tsstill usegetLiteralAsStringfor type-level literal values (not clientDefault). These have the same escaping gap but are pre-existing and out of scope for this PR._FieldNamelocal 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.queryParameter.clientDefault.stringis passed togo.TypeInstantiation.string()which handles escaping. Confirm this is safe for all string contents (quotes, backslashes, newlines).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
tsc) passed via seed test buildgetLiteralAsString→ safegetLiteralValue)x-fern-defaultfixture — generated Go code compiles (go build) and passes tests (go test) ✓exhaustivefixture (both variants) — compiles and passes tests ✓x-fern-default+exhaustiveregeneration)Link to Devin session: https://app.devin.ai/sessions/dae61f87717a46d781e579e47b4758e5
Requested by: @Swimburger