Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
16f27ab
feat(go): add clientDefault support to Go SDK generator
Swimburger Apr 21, 2026
f0a731b
feat(go): add clientDefault support to Go v2 generator
Swimburger Apr 21, 2026
80849a3
fix(go): fix compile errors in v2 query param clientDefault
Swimburger Apr 21, 2026
c806409
fix(go): use local variables for clientDefault to avoid mutating call…
Swimburger Apr 21, 2026
ddc03bc
style(go): fix biome formatting for long line in buildEndpointUrl
Swimburger Apr 21, 2026
14bfeb5
fix(go): update versions.yml createdAt date for 1.35.0 from 2026-04-0…
Swimburger Apr 21, 2026
d1255fb
fix(go): emit clientDefault local vars before endpointURL assignment …
Swimburger Apr 21, 2026
7848ab7
Merge branch 'main' into devin/1776787826-client-default-go
Swimburger Apr 21, 2026
1080c47
fix(go): remove isPlainStringType guard for query param clientDefault…
Swimburger Apr 21, 2026
187a3a5
fix(go): use safe Go identifier for path param clientDefault local va…
Swimburger Apr 21, 2026
0d1bcd9
Merge remote-tracking branch 'origin/main' into devin/1776787826-clie…
devin-ai-integration[bot] Apr 21, 2026
06af2d2
fix(go): use safe getLiteralValue instead of unescaped getLiteralAsSt…
Swimburger Apr 21, 2026
a0829b7
chore: update go-sdk seed snapshots for x-fern-default clientDefault …
Swimburger Apr 21, 2026
6548f65
Merge branch 'main' into devin/1776787826-client-default-go
Swimburger Apr 22, 2026
4459876
Merge branch 'main' into devin/1776787826-client-default-go
Swimburger Apr 22, 2026
a35e871
Merge branch 'main' into devin/1776787826-client-default-go
Swimburger Apr 22, 2026
5dc28ef
Merge branch 'main' into devin/1776787826-client-default-go
Swimburger Apr 22, 2026
d584b11
chore(go): use unreleased changelog mechanism instead of versions.yml…
Swimburger Apr 23, 2026
2e65427
revert generators/go/sdk/versions.yml changes
Swimburger Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions generators/go-v2/sdk/src/authUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ export function getRequestPropertyValueType(requestProperty: FernIr.RequestPrope
return undefined;
}

/**
* Returns true if the given type reference is a non-optional, non-nullable string primitive.
* Used to guard clientDefault generation, since the `== ""` zero-value check
* and string assignment only compile for Go `string` types (not `*string`).
*/
export function isPlainStringType(typeRef: FernIr.TypeReference): boolean {
if (typeRef.type === "container") {
return false;
}
if (typeRef.type === "primitive") {
return typeRef.primitive.v1 === FernIr.PrimitiveTypeV1.String;
}
return false;
}

/**
* Checks if a type reference is optional.
*/
Expand Down
43 changes: 36 additions & 7 deletions generators/go-v2/sdk/src/client/ClientGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getInferredAuthScheme,
getOAuthClientCredentialsScheme,
getRequestPropertyFieldName,
isPlainStringType,
isRequestPropertyOptional,
isTypeReferenceOptional,
resolveTokenEndpointBodyProperties
Expand Down Expand Up @@ -231,14 +232,21 @@ export class ClientGenerator extends FileGenerator<GoFile, SdkCustomConfigSchema

private writeHeaderEnvironmentVariables({ writer }: { writer: go.Writer }): void {
for (const header of this.context.ir.headers) {
if (header.env == null) {
continue;
if (header.env != null) {
this.writeEnvConditional({
writer,
propertyReference: this.getOptionsPropertyReference(header.name),
env: header.env
});
}
// After env fallback, apply clientDefault if present and type is plain string
if (header.clientDefault != null && isPlainStringType(header.valueType)) {
this.writeClientDefaultConditional({
writer,
propertyReference: this.getOptionsPropertyReference(header.name),
clientDefault: header.clientDefault
});
}
this.writeEnvConditional({
writer,
propertyReference: this.getOptionsPropertyReference(header.name),
env: header.env
});
}
}

Expand Down Expand Up @@ -893,6 +901,27 @@ export class ClientGenerator extends FileGenerator<GoFile, SdkCustomConfigSchema
writer.writeLine("}");
}

private writeClientDefaultConditional({
writer,
propertyReference,
clientDefault
}: {
writer: go.Writer;
propertyReference: go.Selector;
clientDefault: FernIr.Literal;
}): void {
writer.write("if ");
writer.writeNode(propertyReference);
writer.writeLine(' == "" {');
writer.indent();
writer.writeNode(propertyReference);
writer.write(" = ");
writer.writeNode(this.context.getLiteralValue(clientDefault));
writer.newLine();
writer.dedent();
writer.writeLine("}");
}

private getOptionsPropertyReference(name: NameInput): go.Selector {
return go.selector({
on: go.codeblock("options"),
Expand Down
87 changes: 81 additions & 6 deletions generators/go-v2/sdk/src/endpoint/http/HttpEndpointGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { assertNever } from "@fern-api/core-utils";
import { go } from "@fern-api/go-ast";
import { FernIr } from "@fern-fern/ir-sdk";

import { isPlainStringType } from "../../authUtils.js";
import { SdkGeneratorContext } from "../../SdkGeneratorContext.js";
import { AbstractEndpointGenerator } from "../AbstractEndpointGenerator.js";
import { EndpointSignatureInfo } from "../EndpointSignatureInfo.js";
Expand Down Expand Up @@ -620,19 +621,48 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
const pathSuffix = this.getPathSuffix({ endpoint });
const baseUrl = pathSuffix.length === 0 ? "baseURL" : `baseURL + "/${pathSuffix}"`;
return go.codeblock((writer) => {
writer.write("endpointURL := ");
if (endpoint.allPathParameters.length === 0) {
writer.write("endpointURL := ");
writer.write(baseUrl);
return;
}
// Apply clientDefault fallback for path parameters before URL encoding.
// Use local variables to avoid mutating the caller's request struct when
// path params come from a wrapped request (e.g., request.Region).
const pathParamLocalVars: Record<string, string> = {};
for (const pathParameter of endpoint.allPathParameters) {
if (pathParameter.clientDefault != null && isPlainStringType(pathParameter.valueType)) {
const ref = signature.pathParameterReferences[getOriginalName(pathParameter.name)];
if (ref != null) {
const localVar = `_${this.context.getFieldName(pathParameter.name)}`;
writer.writeLine(`${localVar} := ${ref}`);
writer.writeLine(`if ${localVar} == "" {`);
writer.indent();
writer.write(`${localVar} = `);
writer.writeNode(this.context.getLiteralValue(pathParameter.clientDefault));
writer.newLine();
writer.dedent();
writer.writeLine("}");
pathParamLocalVars[getOriginalName(pathParameter.name)] = localVar;
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
}
const pathParameterReferences: go.AstNode[] = [];
for (const pathParameter of endpoint.allPathParameters) {
const pathParameterReference = signature.pathParameterReferences[getOriginalName(pathParameter.name)];
const originalName = getOriginalName(pathParameter.name);
// Use the local variable if we created one for clientDefault
const localVar = pathParamLocalVars[originalName];
if (localVar != null) {
pathParameterReferences.push(go.codeblock(localVar));
continue;
}
const pathParameterReference = signature.pathParameterReferences[originalName];
if (pathParameterReference == null) {
continue;
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
pathParameterReferences.push(go.codeblock(pathParameterReference));
}
writer.write("endpointURL := ");
writer.writeNode(this.context.callEncodeUrl([go.codeblock(baseUrl), ...pathParameterReferences]));
});
}
Expand Down Expand Up @@ -725,7 +755,7 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
return undefined;
}

// extract and populate defaults
// extract and populate defaults (from type-level defaults and clientDefault)
const defaults = [];
if (this.context.customConfig.useDefaultRequestParameterValues) {
for (const queryParameter of endpoint.queryParameters) {
Expand All @@ -739,6 +769,33 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
});
}
}
// Track wire values that already have type-level defaults
const defaultedWireValues = new Set<string>();
if (this.context.customConfig.useDefaultRequestParameterValues) {
for (const queryParameter of endpoint.queryParameters) {
if (this.extractDefaultValue(queryParameter.valueType) != null) {
defaultedWireValues.add(getWireValue(queryParameter.name));
}
}
}
// Add clientDefault entries for query params that don't already have a type-level default.
// Unlike path params and headers (which use the == "" zero-value pattern and require
// string types), query params use a defaults map so any literal type works here.
for (const queryParameter of endpoint.queryParameters) {
if (queryParameter.clientDefault != null) {
const wireValue = getWireValue(queryParameter.name);
if (!defaultedWireValues.has(wireValue)) {
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)
});
Comment on lines +788 to +795
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.

}
}
}
const defaults_map = go.TypeInstantiation.map({
keyType: go.Type.string(),
valueType: go.Type.any(),
Expand Down Expand Up @@ -849,9 +906,27 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
writer.writeLine("}");
continue;
}
writer.writeNode(
this.addHeaderValue({ wireValue: getWireValue(header.name), value: format.formatted })
);
// Apply clientDefault fallback for non-optional string headers.
// Use a local variable to avoid mutating the caller's request struct.
if (header.clientDefault != null && isPlainStringType(header.valueType)) {
const localVar = `_${this.context.getFieldName(headerNameVal)}`;
writer.writeNewLineIfLastLineNot();
writer.writeLine(`${localVar} := ${headerField}`);
writer.writeLine(`if ${localVar} == "" {`);
writer.indent();
writer.write(`${localVar} = `);
writer.writeNode(this.context.getLiteralValue(header.clientDefault));
writer.newLine();
writer.dedent();
writer.writeLine("}");
writer.writeNode(
this.addHeaderValue({ wireValue: getWireValue(header.name), value: go.codeblock(localVar) })
);
} else {
writer.writeNode(
this.addHeaderValue({ wireValue: getWireValue(header.name), value: format.formatted })
);
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
const acceptHeader = this.getAcceptHeaderValue({ endpoint });
if (acceptHeader != null) {
Expand Down
56 changes: 52 additions & 4 deletions generators/go/internal/fern/ir/http.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading