Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ce98ee1
feat(python): support omitting username/password from basic auth when…
Swimburger Apr 27, 2026
bb1a410
chore(python): release 5.8.0
github-actions[bot] Apr 27, 2026
c13121e
chore(python): update python-sdk seed (#15394)
fern-support Apr 27, 2026
79ef895
chore(python): update python-sdk seed (#15395)
fern-support Apr 27, 2026
a8250cf
feat(docs): add translations block to docs.yml (#15172)
fern-support Apr 27, 2026
b8e2a40
chore(cli): release 4.97.0
github-actions[bot] Apr 27, 2026
95d708d
fix(java): add required-key guards to undiscriminated union deseriali…
Swimburger Apr 27, 2026
d91fe7b
chore(java): release 4.6.1
github-actions[bot] Apr 27, 2026
95d1484
feat(go): use auth placeholder values in README snippets (#15353)
Swimburger Apr 27, 2026
0c79301
chore(go): release 1.38.0
github-actions[bot] Apr 27, 2026
7cfaa1a
chore(swift): update swift-sdk seed (#15405)
fern-support Apr 27, 2026
da13736
chore(rust): update rust-sdk seed (#15403)
fern-support Apr 27, 2026
d2cdc33
chore(java): update java-sdk seed (#15404)
fern-support Apr 27, 2026
14a2e4b
chore(php): update php-sdk seed (#15402)
fern-support Apr 27, 2026
01ddc75
chore(csharp): update csharp-sdk seed (#15396)
fern-support Apr 27, 2026
a5a2b0c
chore(go): update go-sdk seed (#15398)
fern-support Apr 27, 2026
495ae35
chore(openapi): update openapi seed (#15400)
fern-support Apr 27, 2026
f4d6235
chore(ruby): update ruby-sdk-v2 seed (#15397)
fern-support Apr 27, 2026
4b83d00
chore(python): update python-sdk seed (#15399)
fern-support Apr 27, 2026
bb65d5d
chore(typescript): update ts-sdk seed (#15401)
fern-support Apr 27, 2026
6dd7029
chore(java): update java-sdk seed (#15407)
fern-support Apr 27, 2026
50aeb3f
chore(go): update go-sdk seed (#15406)
fern-support Apr 27, 2026
c288fdb
chore(deps): deduplicate postcss to 8.5.10 to fix CVE-2026-41305 (#15…
github-actions[bot] Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
35 changes: 35 additions & 0 deletions docs-yml.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@
}
]
},
"translations": {
"oneOf": [
{
"type": "array",
"items": {
"$ref": "#/definitions/docs.TranslationConfig"
}
},
{
"type": "null"
}
]
},
"ai-chat": {
"oneOf": [
{
Expand Down Expand Up @@ -4265,6 +4278,28 @@
"tr"
]
},
"docs.TranslationConfig": {
"type": "object",
"properties": {
"lang": {
"$ref": "#/definitions/docs.Language"
},
"default": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"lang"
],
"additionalProperties": false
},
"docs.AIChatModel": {
"type": "string",
"enum": [
Expand Down
23 changes: 23 additions & 0 deletions fern/apis/docs-yml/definition/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ types:
- sv
- tr

TranslationConfig:
properties:
lang: Language
default:
type: optional<boolean>
docs: Whether this language is the default. At most one entry should be marked as default.

AnalyticsConfig:
properties:
segment: optional<SegmentConfig>
Expand Down Expand Up @@ -273,6 +280,22 @@ types:

languages: optional<list<Language>>

translations:
type: optional<list<TranslationConfig>>
docs: |
Configuration for multi-language documentation. Each entry defines a locale
that the documentation supports. Use the `translations/` directory alongside
`docs.yml` to provide per-language content.

Example:
```yaml
translations:
- lang: en
default: true
- lang: ja
- lang: fr
```

# Deprecated
"ai-chat": optional<AIChatConfig>

Expand Down
2 changes: 1 addition & 1 deletion generators/go-v2/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@fern-api/go-ast": "workspace:*",
"@fern-api/logging-execa": "workspace:*",
"@fern-fern/generator-exec-sdk": "catalog:",
"@fern-fern/ir-sdk": "66.0.0",
"@fern-fern/ir-sdk": "66.3.0",
"@types/node": "catalog:",
"dedent": "catalog:",
"typescript": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion generators/go-v2/model/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@fern-api/fs-utils": "workspace:*",
"@fern-api/go-ast": "workspace:*",
"@fern-api/go-base": "workspace:*",
"@fern-fern/ir-sdk": "66.0.0",
"@fern-fern/ir-sdk": "66.3.0",
"@types/node": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
Expand Down
6 changes: 3 additions & 3 deletions generators/go-v2/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@fern-api/base-generator": "workspace:*",
"@fern-api/configs": "workspace:*",
"@fern-api/core-utils": "workspace:*",
"@fern-api/dynamic-ir-sdk": "66.1.0",
"@fern-api/fs-utils": "workspace:*",
"@fern-api/go-ast": "workspace:*",
"@fern-api/go-base": "workspace:*",
Expand All @@ -50,11 +51,10 @@
"@fern-api/mock-utils": "workspace:*",
"@fern-fern/generator-cli-sdk": "^0.1.5",
"@fern-fern/generator-exec-sdk": "catalog:",
"@fern-fern/ir-sdk": "66.0.0",
"@fern-fern/ir-sdk": "66.3.0",
"@types/node": "catalog:",
"dedent": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:",
"@fern-api/dynamic-ir-sdk": "66.1.0"
"vitest": "catalog:"
}
}
32 changes: 27 additions & 5 deletions generators/go-v2/sdk/src/readme/ReadmeSnippetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder {
return this.writeCode(dedent`
// Specify default options applied on every request.
${ReadmeSnippetBuilder.CLIENT_VARIABLE_NAME} := ${this.rootPackageClientName}.NewClient(
option.WithToken("<YOUR_API_KEY>"),
option.WithToken("${this.getTokenPlaceholder()}"),
option.WithHTTPClient(
&http.Client{
Timeout: 5 * time.Second,
Expand All @@ -190,7 +190,7 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder {
// Specify options for an individual request.
response, err := ${this.getMethodCall(endpoint)}(
...,
option.WithToken("<YOUR_API_KEY>"),
option.WithToken("${this.getTokenPlaceholder()}"),
)
`);
}
Expand Down Expand Up @@ -303,6 +303,28 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder {
`);
}

private getTokenPlaceholder({ defaultValue = "<YOUR_API_KEY>" }: { defaultValue?: string } = {}): string {
if (this.context.ir.auth != null) {
for (const scheme of this.context.ir.auth.schemes) {
if (scheme.type === "bearer" && scheme.tokenPlaceholder != null) {
return scheme.tokenPlaceholder;
}
if (scheme.type === "header" && scheme.headerPlaceholder != null) {
return scheme.headerPlaceholder;
}
}
}
return defaultValue;
}

private getOAuthClientIdPlaceholder(): string {
return "<YOUR_CLIENT_ID>";
}

private getOAuthClientSecretPlaceholder(): string {
return "<YOUR_CLIENT_SECRET>";
}

private hasOAuthScheme(): boolean {
if (this.context.ir.auth == null) {
return false;
Expand All @@ -315,14 +337,14 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder {
// Option 1: Use client credentials (SDK will handle token fetching and refresh)
${ReadmeSnippetBuilder.CLIENT_VARIABLE_NAME} := ${this.rootPackageClientName}.NewClient(
option.WithClientCredentials(
"<YOUR_CLIENT_ID>",
"<YOUR_CLIENT_SECRET>",
"${this.getOAuthClientIdPlaceholder()}",
"${this.getOAuthClientSecretPlaceholder()}",
),
)

// Option 2: Use a pre-fetched token directly
${ReadmeSnippetBuilder.CLIENT_VARIABLE_NAME} := ${this.rootPackageClientName}.NewClient(
option.WithToken("<YOUR_ACCESS_TOKEN>"),
option.WithToken("${this.getTokenPlaceholder({ defaultValue: "<YOUR_ACCESS_TOKEN>" })}"),
)
`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export class WireTestSetupGenerator {
}

public static getWiremockConfigContent(ir: FernIr.IntermediateRepresentation) {
return new WireMock().convertToWireMock(ir);
// ir-sdk versions may differ between go-sdk (66.3.0) and mock-utils (66.0.0);
// 66.3.0 is a strict superset (adds optional fields only), so this is safe.
return new WireMock().convertToWireMock(ir as Parameters<WireMock["convertToWireMock"]>[0]);
}

private generateWireMockConfigFile(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- summary: |
Use auth scheme placeholder values in README snippets when configured via
`placeholder` field on auth schemes.
type: feat
8 changes: 8 additions & 0 deletions generators/go/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.38.0
changelogEntry:
- summary: |
Use auth scheme placeholder values in README snippets when configured via
`placeholder` field on auth schemes.
type: feat
createdAt: "2026-04-27"
irVersion: 66
- version: 1.37.0
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import com.fern.ir.model.types.ContainerType;
import com.fern.ir.model.types.DeclaredTypeName;
import com.fern.ir.model.types.Literal;
import com.fern.ir.model.types.ObjectProperty;
import com.fern.ir.model.types.ObjectTypeDeclaration;
import com.fern.ir.model.types.PrimitiveType;
import com.fern.ir.model.types.PrimitiveTypeV1;
import com.fern.ir.model.types.TypeDeclaration;
Expand All @@ -54,6 +56,7 @@
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -604,6 +607,28 @@ private TypeSpec getDeserializer() {
if (typeName.isPrimitive() || typeName.isBoxedPrimitive()) {
continue;
}
List<String> requiredKeys = getRequiredWireKeys(member);
boolean hasRequiredKeyGuard = !requiredKeys.isEmpty();
if (hasRequiredKeyGuard) {
ParameterizedTypeName mapWildcard = ParameterizedTypeName.get(
ClassName.get(Map.class),
WildcardTypeName.subtypeOf(Object.class),
WildcardTypeName.subtypeOf(Object.class));
StringBuilder guard = new StringBuilder();
guard.append("$L instanceof $T");
for (String key : requiredKeys) {
guard.append(" && (($T) $L).containsKey($S)");
}
List<Object> guardArgs = new ArrayList<>();
guardArgs.add(VALUE_FIELD_SPEC.name);
guardArgs.add(mapWildcard);
for (String key : requiredKeys) {
guardArgs.add(mapWildcard);
guardArgs.add(VALUE_FIELD_SPEC.name);
guardArgs.add(key);
}
deserializeMethod.beginControlFlow("if (" + guard.toString() + ")", guardArgs.toArray());
}
if (shouldDeserializeWithTypeReference(member)) {
deserializeMethod
.beginControlFlow("try")
Expand All @@ -629,6 +654,9 @@ private TypeSpec getDeserializer() {
.nextControlFlow("catch($T e)", RuntimeException.class)
.endControlFlow();
}
if (hasRequiredKeyGuard) {
deserializeMethod.endControlFlow();
}
}
deserializeMethod.addStatement("throw new $T(p, $S)", JsonParseException.class, "Failed to deserialize");
return deserializerBuilder.addMethod(deserializeMethod.build()).build();
Expand Down Expand Up @@ -659,6 +687,76 @@ private boolean shouldDeserializeWithTypeReference(UndiscriminatedUnionMember me
return false;
}

/**
* Returns the wire keys of all required (non-optional, non-nullable, non-literal) properties for an object-typed
* union member. Returns an empty list for non-object members. This is used to emit key-presence guards in the
* deserializer so that trial-order deserialization doesn't misclassify payloads when the Jackson builder silently
* accepts missing fields.
*/
private List<String> getRequiredWireKeys(UndiscriminatedUnionMember member) {
if (!member.getType().isNamed()) {
return Collections.emptyList();
}
TypeId typeId = member.getType().getNamed().get().getTypeId();
// Walk alias chains until we hit a non-alias type declaration. Guards against alias cycles
// with a visited-set keyed by TypeId.
Set<TypeId> visited = new HashSet<>();
while (typeId != null && visited.add(typeId)) {
TypeDeclaration typeDeclaration =
generatorContext.getTypeDeclarations().get(typeId);
if (typeDeclaration == null) {
return Collections.emptyList();
}
if (typeDeclaration.getShape().isObject()) {
ObjectTypeDeclaration objectType =
typeDeclaration.getShape().getObject().get();
List<String> requiredKeys = new ArrayList<>();
collectRequiredWireKeys(objectType, requiredKeys);
return requiredKeys;
}
if (!typeDeclaration.getShape().isAlias()) {
// Enum / union / undiscriminated union / etc. — convertValue will reject a JSON
// object naturally for these, so no presence guard is needed.
return Collections.emptyList();
}
com.fern.ir.model.types.ResolvedTypeReference resolved =
typeDeclaration.getShape().getAlias().get().getResolvedType();
if (!resolved.isNamed()) {
// Resolved to primitive / container / unknown — not an object shape.
return Collections.emptyList();
}
typeId = resolved.getNamed().get().getName().getTypeId();
}
return Collections.emptyList();
}

private void collectRequiredWireKeys(ObjectTypeDeclaration objectType, List<String> requiredKeys) {
for (ObjectProperty property : objectType.getProperties()) {
if (isRequiredProperty(property)) {
requiredKeys.add(NameUtils.getWireValue(property.getName()));
}
}
// Include required properties from extended types
for (DeclaredTypeName extendedType : objectType.getExtends()) {
TypeDeclaration extDeclaration =
generatorContext.getTypeDeclarations().get(extendedType.getTypeId());
if (extDeclaration != null && extDeclaration.getShape().isObject()) {
collectRequiredWireKeys(extDeclaration.getShape().getObject().get(), requiredKeys);
}
}
}

private static boolean isRequiredProperty(ObjectProperty property) {
com.fern.ir.model.types.TypeReference valueType = property.getValueType();
if (valueType.isContainer()) {
ContainerType container = valueType.getContainer().get();
if (container.isOptional() || container.isNullable() || container.isLiteral()) {
return false;
}
}
return true;
}

private static String visitorName(Set<String> reservedTypeNames) {
return reservedTypeNames.contains(VISITOR_CLASS_NAME) ? VISITOR_CLASS_NAME_UNDERSCORE : VISITOR_CLASS_NAME;
}
Expand Down
Loading
Loading