Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
59e7ab7
feat(cli): add --id flag to `fern docs preview delete` command (#14941)
iamnamananand996 Apr 15, 2026
882a8a4
chore(cli): release 4.71.0
github-actions[bot] Apr 15, 2026
74d09e5
chore(java): fix 3 high container vulnerabilities in java-sdk (#15021)
github-actions[bot] Apr 15, 2026
6ae7ba4
feat(cli): improve changelog formatting, version headers, and 5-line …
Swimburger Apr 15, 2026
266c51a
fix(cli): avoid missing-redirect warnings when old URLs are served by…
kafkas Apr 15, 2026
8dc7991
chore(cli): release 4.71.1
github-actions[bot] Apr 15, 2026
b9108d4
feat(ci): handle pre-release versions and add pre-release tag support…
Swimburger Apr 15, 2026
6dfd6a7
chore(java): release 4.2.0
github-actions[bot] Apr 15, 2026
bff9e50
fix(go): preserve SDK version in dynamic snippets instead of using ge…
davidkonigsberg Apr 15, 2026
5901753
fix(cli): resolve allOf composition bugs in V3 OpenAPI importer (#14873)
devin-ai-integration[bot] Apr 15, 2026
0ba2048
chore(cli): release 4.71.2
github-actions[bot] Apr 15, 2026
41d5c2d
chore(go): release 1.34.4
github-actions[bot] Apr 15, 2026
48968cd
chore(rust): update rust-sdk seed (#15031)
fern-support Apr 15, 2026
f746070
chore(openapi): update openapi seed (#15032)
fern-support Apr 15, 2026
f9500b0
chore(go): update go-model seed (#15037)
fern-support Apr 15, 2026
567ce1b
chore(python): update python-sdk seed (#15036)
fern-support Apr 15, 2026
0e37460
chore(rust): update rust-model seed (#15034)
fern-support Apr 15, 2026
8a1be44
chore(php): update php-sdk seed (#15035)
fern-support Apr 15, 2026
eac269b
chore(csharp): update csharp-model seed (#15033)
fern-support Apr 15, 2026
3886d10
chore(ruby): update ruby-sdk-v2 seed (#15041)
fern-support Apr 15, 2026
71477b0
chore(csharp): update csharp-sdk seed (#15040)
fern-support Apr 15, 2026
f555a12
chore(java): update java-model seed (#15039)
fern-support Apr 15, 2026
c133fdb
chore(go): update go-sdk seed (#15038)
fern-support Apr 15, 2026
76d6283
chore(java): update java-sdk seed (#15030)
fern-support Apr 15, 2026
82e39af
chore(python): update pydantic seed (#15044)
fern-support Apr 15, 2026
3c0cdad
chore(php): update php-model seed (#15045)
fern-support Apr 15, 2026
df1a801
chore(swift): update swift-sdk seed (#15043)
fern-support Apr 15, 2026
1e56393
chore(typescript): update ts-sdk seed (#15042)
fern-support Apr 15, 2026
0289181
fix(java): wire test potpourri (#15003)
patrickthornton Apr 15, 2026
595d39f
chore(rust): update rust-model seed (#15047)
fern-support Apr 15, 2026
87aa3b4
fix(seed): ignore changelog/changes directories in affected detection…
Swimburger Apr 15, 2026
861e8d0
chore(java): update java-sdk seed (#15050)
fern-support Apr 15, 2026
e9ef8ce
chore(java): update java-model seed (#15051)
fern-support Apr 15, 2026
8e34ab5
fix(typescript): support inferred OAuth auth with form-encoded token …
Swimburger Apr 15, 2026
a433c58
fix(python): handle OAuth and header auth simultaneously (#15029)
patrickthornton Apr 15, 2026
ea45cce
chore(typescript): release 3.63.5
github-actions[bot] Apr 15, 2026
e66ea82
chore(java): update java-model seed (#15052)
fern-support Apr 15, 2026
de4a89b
chore(java): update java-sdk seed (#15053)
fern-support Apr 15, 2026
0a82214
chore(typescript): update ts-sdk seed (#15056)
fern-support Apr 15, 2026
90479c9
chore(typescript): update ts-sdk seed (#15058)
fern-support Apr 15, 2026
235bf90
fix(go): use go `1.26.2` in dockerfiles (#15055)
patrickthornton Apr 15, 2026
51f3184
chore(go): release 1.34.5
github-actions[bot] Apr 15, 2026
8790f61
chore(go): update go-model seed (#15059)
fern-support Apr 15, 2026
75aca3d
chore(go): update go-sdk seed (#15060)
fern-support Apr 15, 2026
83c841b
chore(go): update go-model seed (#15063)
fern-support Apr 15, 2026
a308fbf
chore(go): update go-sdk seed (#15062)
fern-support Apr 15, 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
  •  
  •  
  •  
14 changes: 14 additions & 0 deletions fern-changes-yml.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"type": {
"$ref": "#/definitions/ChangelogType",
"description": "Type of change determines version bump: fix/chore (patch), feat/internal (minor), break (major)"
},
"pre-release": {
"$ref": "#/definitions/PreReleaseTag",
"description": "Optional pre-release tag. When set, the release version includes this suffix (e.g., 1.2.0-rc.0)."
}
},
"required": [
Expand All @@ -37,6 +41,16 @@
],
"title": "Changelog Type",
"description": "The type of change: fix/chore → patch, feat/internal → minor, break → major"
},
"PreReleaseTag": {
"type": "string",
"enum": [
"alpha",
"beta",
"rc"
],
"title": "Pre-Release Tag",
"description": "Pre-release identifier appended to the version (e.g., rc → 1.2.0-rc.0)"
}
}
}
1 change: 1 addition & 0 deletions generators/csharp/sdk/changes/unreleased/.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
Brief description of your change.
You can use multiple lines for longer descriptions.
type: feat # Options: fix/chore (patch), feat/internal (minor), break (major)
# pre-release: rc # Optional: alpha, beta, or rc (e.g., produces 1.2.0-rc.0)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AbstractDynamicSnippetsGeneratorContext,
type FernGeneratorExec
type FernGeneratorExec,
getSdkVersion
} from "@fern-api/browser-compatible-base-generator";
import { assertNever } from "@fern-api/core-utils";
import type { FernIr } from "@fern-api/dynamic-ir-sdk";
Expand Down Expand Up @@ -214,14 +215,16 @@ export class DynamicSnippetsGeneratorContext extends AbstractDynamicSnippetsGene
if (publishInfo.type !== "go") {
return config;
}
// Prefer --version flag over IR's publish version (which may be the generator version, not SDK version).
const originalVersion = getSdkVersion(config);
return {
...config,
customConfig: generatorConfig.customConfig ?? config.customConfig,
output: {
...config.output,
mode: {
type: "github",
version: publishInfo.version,
version: originalVersion ?? publishInfo.version,
repoUrl: publishInfo.repoUrl
} as FernGeneratorExec.OutputMode
}
Expand Down
2 changes: 1 addition & 1 deletion generators/go/model/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM node:22.12-alpine3.20 AS node

FROM golang:1.23.8-alpine3.20
FROM golang:1.26.2-alpine3.23

ENV YARN_CACHE_FOLDER=/.yarn

Expand Down
2 changes: 1 addition & 1 deletion generators/go/sdk/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ RUN git config --global user.email "115122769+fern-api[bot]@users.noreply.github
COPY generators/go-v2/sdk/dist/ /dist/

# Stage 2: Final Go image
FROM golang:1.23.8-alpine3.20
FROM golang:1.26.2-alpine3.23

WORKDIR /workspace

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- summary: |
Fix dynamic snippets using the generator version instead of the SDK version
for resolving Go import paths, which caused incorrect major version suffixes
in wire test imports and broke go mod tidy.
type: fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Bump the Go toolchain in the generator Docker image from 1.23.8 to 1.26.2
(alpine3.23). This unblocks `go mod tidy` for generated SDKs whose modules
(or transitive dependencies) require a newer Go version, which previously
failed with `go.mod requires go >= X (running go 1.23.8; GOTOOLCHAIN=local)`.
type: fix
1 change: 1 addition & 0 deletions generators/go/sdk/changes/unreleased/.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
Brief description of your change.
You can use multiple lines for longer descriptions.
type: feat # Options: fix/chore (patch), feat/internal (minor), break (major)
# pre-release: rc # Optional: alpha, beta, or rc (e.g., produces 1.2.0-rc.0)
19 changes: 19 additions & 0 deletions generators/go/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.34.5
changelogEntry:
- summary: |
Bump the Go toolchain in the generator Docker image from 1.23.8 to 1.26.2
(alpine3.23). This unblocks `go mod tidy` for generated SDKs whose modules
(or transitive dependencies) require a newer Go version, which previously
failed with `go.mod requires go >= X (running go 1.23.8; GOTOOLCHAIN=local)`.
type: fix
createdAt: "2026-04-15"
irVersion: 66
- version: 1.34.4
changelogEntry:
- summary: |
Fix dynamic snippets using the generator version instead of the SDK version
for resolving Go import paths, which caused incorrect major version suffixes
in wire test imports and broke go mod tidy.
type: fix
createdAt: "2026-04-15"
irVersion: 66
- version: 1.34.3
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,8 @@ export class DynamicTypeLiteralMapper {
case "discriminatedUnion":
return this.convertDiscriminatedUnion({
discriminatedUnion: named,
value
value,
as
});
case "enum":
return this.convertEnum({ enum_: named, value });
Expand All @@ -330,18 +331,20 @@ export class DynamicTypeLiteralMapper {
case "undiscriminatedUnion":
// Don't pass inUndiscriminatedUnion here - we're AT the undiscriminated union level,
// not within it. The flag should only apply to the variants within the union.
return this.convertUndiscriminatedUnion({ undiscriminatedUnion: named, value });
return this.convertUndiscriminatedUnion({ undiscriminatedUnion: named, value, as });
default:
assertNever(named);
}
}

private convertDiscriminatedUnion({
discriminatedUnion,
value
value,
as
}: {
discriminatedUnion: FernIr.dynamic.DiscriminatedUnionType;
value: unknown;
as?: DynamicTypeLiteralMapper.ConvertedAs;
}): java.TypeLiteral {
const classReference = this.context.getJavaClassReferenceFromDeclaration({
declaration: discriminatedUnion.declaration
Expand All @@ -366,7 +369,7 @@ export class DynamicTypeLiteralMapper {
java.invokeMethod({
on: classReference,
method: this.context.getPropertyName(unionVariant.discriminantValue.name),
arguments_: [this.convertNamed({ named, value: discriminatedUnionTypeInstance.value })]
arguments_: [this.convertNamed({ named, value: discriminatedUnionTypeInstance.value, as })]
})
);
}
Expand All @@ -386,7 +389,8 @@ export class DynamicTypeLiteralMapper {
arguments_: [
this.convert({
typeReference: unionVariant.typeReference,
value: record[propertyKey]
value: record[propertyKey],
as
})
]
})
Expand Down Expand Up @@ -646,14 +650,17 @@ export class DynamicTypeLiteralMapper {

private convertUndiscriminatedUnion({
undiscriminatedUnion,
value
value,
as
}: {
undiscriminatedUnion: FernIr.dynamic.UndiscriminatedUnionType;
value: unknown;
as?: DynamicTypeLiteralMapper.ConvertedAs;
}): java.TypeLiteral {
const result = this.findMatchingUndiscriminatedUnionType({
undiscriminatedUnion,
value
value,
as
});
if (result == null) {
return java.TypeLiteral.nop();
Expand Down Expand Up @@ -686,17 +693,20 @@ export class DynamicTypeLiteralMapper {

private findMatchingUndiscriminatedUnionType({
undiscriminatedUnion,
value
value,
as
}: {
undiscriminatedUnion: FernIr.dynamic.UndiscriminatedUnionType;
value: unknown;
as?: DynamicTypeLiteralMapper.ConvertedAs;
}): { valueTypeReference: FernIr.dynamic.TypeReference; typeInstantiation: java.TypeLiteral } | undefined {
for (const typeReference of undiscriminatedUnion.types) {
const errorsBefore = this.context.errors.size();
try {
const typeInstantiation = this.convert({
typeReference,
value,
as,
inUndiscriminatedUnion: true
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ export class TestClassBuilder {
writer.indent();
writer.writeLine("java.util.Map.Entry<String, JsonNode> entry = iter.next();");
writer.writeLine("JsonNode actualValue = actual.get(entry.getKey());");
writer.writeLine("if (actualValue == null || !jsonEquals(entry.getValue(), actualValue)) return false;");
writer.writeLine("if (actualValue == null) { if (!entry.getValue().isNull()) return false; }");
writer.writeLine("else if (!jsonEquals(entry.getValue(), actualValue)) return false;");
writer.dedent();
writer.writeLine("}");
writer.writeLine("return true;");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,15 @@ export class TestMethodBuilder {
const rawResponseJson = testExample.response.body;
const responseStatusCode = testExample.response.statusCode;

// Convert RFC 2822 dates to ISO 8601 in the response JSON BEFORE using it for both
// the mock response body (served by MockWebServer) and the expected response assertion.
// Jackson's JavaTimeModule expects ISO 8601 for OffsetDateTime fields typed as "dateTime";
// RFC 2822 dates would cause DateTimeParseException during deserialization.
// Normalize dates in the response JSON to match Jackson's round-trip output:
// - Convert RFC 2822 dates to ISO 8601 (Jackson expects ISO for OffsetDateTime)
// - Append "Z" to timezone-less datetimes (DateTimeDeserializer defaults to UTC)
const expectedResponseJson = rawResponseJson
? (this.convertRfc2822DatesToIso8601(rawResponseJson) as typeof rawResponseJson)
? (this.transformJsonStrings(rawResponseJson, (s) => {
const converted = this.tryConvertRfc2822Date(s);
// Append Z to timezone-less ISO 8601 datetimes
return converted.replace(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/, "$1Z");
}) as typeof rawResponseJson)
: rawResponseJson;

const mockResponseBody = expectedResponseJson
Expand Down Expand Up @@ -309,22 +312,19 @@ export class TestMethodBuilder {
}

/**
* Recursively converts RFC 2822 date strings to ISO 8601 format with Z suffix
* in JSON data. This is needed because Jackson's JavaTimeModule serializes
* OffsetDateTime as ISO 8601 (e.g. "2015-07-30T20:00:00Z"), but the IR example
* data may contain RFC 2822 dates (e.g. "Thu, 30 Jul 2015 20:00:00 +0000").
* Recursively applies a string transform to all string values in a JSON structure.
*/
private convertRfc2822DatesToIso8601(data: unknown): unknown {
private transformJsonStrings(data: unknown, transform: (s: string) => string): unknown {
if (typeof data === "string") {
return this.tryConvertRfc2822Date(data);
return transform(data);
}
if (Array.isArray(data)) {
return data.map((item) => this.convertRfc2822DatesToIso8601(item));
return data.map((item) => this.transformJsonStrings(item, transform));
}
if (typeof data === "object" && data !== null) {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(data)) {
result[key] = this.convertRfc2822DatesToIso8601(value);
result[key] = this.transformJsonStrings(value, transform);
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,42 +583,51 @@ private TypeSpec getDeserializer() {
.addAnnotation(ClassName.get("", "java.lang.Override"))
.addStatement(
"$T $L = $L.readValueAs($T.class)", Object.class, VALUE_FIELD_SPEC.name, "p", Object.class);
// Process primitive instanceof checks first — they are exact type matches.
// convertValue branches (used for non-primitives) can do lossy coercion
// (e.g., Integer → String), so they must come after primitives to avoid
// capturing values meant for a more specific primitive variant.
for (int i = 0; i < membersWithoutLiterals.size(); ++i) {
UndiscriminatedUnionMember member = membersWithoutLiterals.get(i);

TypeName typeName = memberTypeNames.get(member);
if (typeName.isPrimitive() || typeName.isBoxedPrimitive()) {
deserializeMethod
.beginControlFlow("if ($L instanceof $T)", VALUE_FIELD_SPEC.name, typeName.box())
.addStatement(
"return $L(($T) $L)", STATIC_FACTORY_METHOD_NAME, typeName.box(), VALUE_FIELD_SPEC.name)
.endControlFlow();
}
}
for (int i = 0; i < membersWithoutLiterals.size(); ++i) {
UndiscriminatedUnionMember member = membersWithoutLiterals.get(i);
TypeName typeName = memberTypeNames.get(member);
if (typeName.isPrimitive() || typeName.isBoxedPrimitive()) {
continue;
}
if (shouldDeserializeWithTypeReference(member)) {
deserializeMethod
.beginControlFlow("try")
.addStatement(
"return $L($T.$N.convertValue($L, new $T() {}))",
getDeConflictedMemberName(member, STATIC_FACTORY_METHOD_NAME),
generatorContext.getPoetClassNameFactory().getObjectMapperClassName(),
ObjectMappersGenerator.JSON_MAPPER_STATIC_FIELD_NAME,
VALUE_FIELD_SPEC.name,
ParameterizedTypeName.get(ClassName.get(TypeReference.class), typeName))
.nextControlFlow("catch($T e)", RuntimeException.class)
.endControlFlow();
} else {
if (shouldDeserializeWithTypeReference(member)) {
deserializeMethod
.beginControlFlow("try")
.addStatement(
"return $L($T.$N.convertValue($L, new $T() {}))",
getDeConflictedMemberName(member, STATIC_FACTORY_METHOD_NAME),
generatorContext.getPoetClassNameFactory().getObjectMapperClassName(),
ObjectMappersGenerator.JSON_MAPPER_STATIC_FIELD_NAME,
VALUE_FIELD_SPEC.name,
ParameterizedTypeName.get(ClassName.get(TypeReference.class), typeName))
.nextControlFlow("catch($T e)", RuntimeException.class)
.endControlFlow();
} else {
deserializeMethod
.beginControlFlow("try")
.addStatement(
"return $L($T.$N.convertValue($L, $T.class))",
getDeConflictedMemberName(member, STATIC_FACTORY_METHOD_NAME),
generatorContext.getPoetClassNameFactory().getObjectMapperClassName(),
ObjectMappersGenerator.JSON_MAPPER_STATIC_FIELD_NAME,
VALUE_FIELD_SPEC.name,
typeName)
.nextControlFlow("catch($T e)", RuntimeException.class)
.endControlFlow();
}
deserializeMethod
.beginControlFlow("try")
.addStatement(
"return $L($T.$N.convertValue($L, $T.class))",
getDeConflictedMemberName(member, STATIC_FACTORY_METHOD_NAME),
generatorContext.getPoetClassNameFactory().getObjectMapperClassName(),
ObjectMappersGenerator.JSON_MAPPER_STATIC_FIELD_NAME,
VALUE_FIELD_SPEC.name,
typeName)
.nextControlFlow("catch($T e)", RuntimeException.class)
.endControlFlow();
}
}
deserializeMethod.addStatement("throw new $T(p, $S)", JsonParseException.class, "Failed to deserialize");
Expand Down
9 changes: 8 additions & 1 deletion generators/java/sdk/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ RUN rm -f /opt/gradle/lib/jackson-core-*.jar && \
# CVE-2025-14819, CVE-2025-15079, CVE-2025-15224), alsa-lib (CVE-2026-25068),
# expat (CVE-2026-25210), openssh/openssh-clients (CVE-2025-61984, CVE-2025-61985)
# Security update 2026-03-30: add freetype (CVE-2026-23865, ALAS2023-2026-1486)
# Security update 2026-04-15: add python3/python3-libs (ALAS2023-2026-1583,
# CVE-2025-11468, CVE-2025-15282, CVE-2026-0672, CVE-2026-0865, CVE-2026-1299,
# CVE-2026-2297, CVE-2026-4224, CVE-2026-4519),
# libnghttp2 (ALAS2023-2026-1542, CVE-2026-27135)
RUN dnf --releasever=latest update -y \
openssl-fips-provider-latest \
openssl-libs \
Expand All @@ -69,7 +73,10 @@ RUN dnf --releasever=latest update -y \
expat \
openssh \
openssh-clients \
freetype && \
freetype \
python3 \
python3-libs \
libnghttp2 && \
dnf remove -y git-lfs wget || true && \
dnf clean all && \
rm -rf /var/cache/dnf
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Fix 3 high container vulnerabilities: update python3/python3-libs to
3.9.25-1.amzn2023.0.4 (ALAS2023-2026-1583) and libnghttp2 to
1.59.0-3.amzn2023.0.2 (ALAS2023-2026-1542, CVE-2026-27135).
type: chore
1 change: 1 addition & 0 deletions generators/java/sdk/changes/unreleased/.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
Brief description of your change.
You can use multiple lines for longer descriptions.
type: feat # Options: fix/chore (patch), feat/internal (minor), break (major)
# pre-release: rc # Optional: alpha, beta, or rc (e.g., produces 1.2.0-rc.0)
Loading
Loading