Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions fern.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
}
]
},
"webhook-signature": {
"oneOf": [
{
"$ref": "#/definitions/webhooks.WebhookSignatureSchema"
},
{
"type": "null"
}
]
},
"channel": {
"oneOf": [
{
Expand Down Expand Up @@ -3950,6 +3960,16 @@
"path": {
"type": "string"
},
"connect-method-name": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"headers": {
"oneOf": [
{
Expand Down
5 changes: 5 additions & 0 deletions fern/apis/fern-definition/definition/file.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ types:
service: optional<service.HttpServiceSchema>
errors: optional<map<string, errors.ErrorDeclarationSchema>>
webhooks: optional<map<string, webhooks.WebhookSchema>>
webhook-signature:
type: optional<webhooks.WebhookSignatureSchema>
docs: |
Default webhook signature verification configuration.
Applied to all webhooks in this file that do not declare their own signature.
channel: optional<websocket.WebSocketChannelSchema>

## Package marker file ##
Expand Down
1 change: 1 addition & 0 deletions fern/apis/fern-definition/definition/websocket.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ types:
auth: boolean
url: optional<string>
path: string
connect-method-name: optional<string>
headers: optional<map<string, service.HttpHeaderSchema>>
path-parameters: optional<map<string, service.HttpPathParameterSchema>>
query-parameters: optional<map<string, service.HttpQueryParameterSchema>>
Expand Down
27 changes: 17 additions & 10 deletions generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,12 @@ export class EndpointSnippetGenerator {
go.invokeMethod({
on: go.codeblock(CLIENT_VAR_NAME),
method: this.getMethod({ endpoint }),
arguments_: [this.context.getContextTodoFunctionInvocation(), ...otherArgs, go.codeblock("nil")]
arguments_: [
this.context.getContextTodoFunctionInvocation(),
...otherArgs,
go.codeblock("nil"),
...optionArgsInvocation
]
})
);
} else {
Expand Down Expand Up @@ -636,14 +641,16 @@ export class EndpointSnippetGenerator {
}

private getBytesBodyRequestArg({ value }: { value: unknown }): go.TypeInstantiation {
if (typeof value !== "string") {
this.context.errors.add({
severity: Severity.Critical,
message: `Expected bytes value to be a string, got ${typeof value}`
});
return go.TypeInstantiation.nop();
}
return go.TypeInstantiation.bytes(value as string);
const bytesValue = typeof value === "string" ? (value as string) : "";
return go.TypeInstantiation.reference(
go.invokeFunc({
func: go.typeReference({
name: "NewReader",
importPath: "bytes"
}),
arguments_: [go.TypeInstantiation.bytes(bytesValue)]
})
);
}

private getMethodArgsForInlinedRequest({
Expand Down Expand Up @@ -872,7 +879,7 @@ export class EndpointSnippetGenerator {
}): go.StructField[] {
const args: go.StructField[] = [];

const pathParameters = this.context.associateByWireValue({
const pathParameters = this.context.associateByWireValueOrDefault({
parameters: namedParameters,
values: snippet.pathParameters ?? {}
});
Expand Down
11 changes: 11 additions & 0 deletions generators/go-v2/sdk/src/wire-tests/WireTestGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,17 @@ export class WireTestGenerator {
basePath = basePath.replace(`{${paramName}}`, paramValue.equalTo);
});

// If there are still unresolved path parameter placeholders (e.g., when the example
// doesn't provide path parameter values), substitute them with the same default values
// that associateByWireValueOrDefault synthesizes (i.e., the parameter name itself).
// This ensures the VerifyRequestCount URL matches what the client actually sends.
for (const part of endpoint.fullPath.parts) {
const paramName = part.pathParameter;
if (paramName && basePath.includes(`{${paramName}}`)) {
basePath = basePath.replace(`{${paramName}}`, `%3C${paramName}%3E`);
}
}

return basePath;
}

Expand Down
16 changes: 16 additions & 0 deletions generators/go/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.25.0-rc3
changelogEntry:
- summary: |
Fix dynamic snippet and wire test generation for endpoints with bytes body and path
parameters. Previously, the generator produced `nil` for the `io.Reader` body argument
instead of a valid `bytes.NewReader([]byte(...))` call, causing wire test failures.
Also fixed the nop request branch to include option arguments (e.g. test ID headers)
in the method invocation, fixed path parameter resolution to use
associateByWireValueOrDefault so missing path parameter values are synthesized, and
fixed WireMock stub response generation to return an empty JSON object `{}` instead
of a JSON string `""` when the example response body is null but the endpoint declares
a named (struct) response type.
type: fix
createdAt: "2026-02-24"
irVersion: 61

- version: 1.25.0-rc2
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ export class EndpointSnippetGenerator {
}
return [
{
name: this.context.getPropertyName(body.bodyKey),
name: REQUEST_BODY_ARG_NAME,
value: typeInstantiation
}
];
Expand Down
6 changes: 6 additions & 0 deletions generators/python-v2/sdk/src/wire-tests/WireTestGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ export class WireTestGenerator {
continue;
}

// Skip bytes request body endpoints — the IR example system has no
// ExampleRequestBody variant for bytes, so we can't produce a proper example.
if (endpoint.requestBody?.type === "bytes") {
continue;
}

// Always use static IR examples to match WireMock mappings
// WireMock mappings are generated from static IR examples, so we must use the same examples
const staticExample = this.getStaticIrExample(endpoint);
Expand Down
9 changes: 9 additions & 0 deletions generators/python/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
# For unreleased changes, use unreleased.yml
- version: 4.59.3
changelogEntry:
- summary: Fix bytes body snippet generation to use `request` parameter name instead of dynamic IR body key.
type: fix
- summary: Skip wire test generation for bytes request body endpoints, which lack IR example support.
type: fix
createdAt: "2026-02-24"
irVersion: 65

- version: 4.59.2
changelogEntry:
- summary: |
Expand Down
20 changes: 20 additions & 0 deletions package-yml.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@
}
]
},
"webhook-signature": {
"oneOf": [
{
"$ref": "#/definitions/webhooks.WebhookSignatureSchema"
},
{
"type": "null"
}
]
},
"channel": {
"oneOf": [
{
Expand Down Expand Up @@ -3970,6 +3980,16 @@
"path": {
"type": "string"
},
"connect-method-name": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"headers": {
"oneOf": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class ChannelConverter2_X extends AbstractChannelConverter<AsyncAPIV2.Cha
channel: {
name: this.context.casingsGenerator.generateName(groupName),
displayName,
connectMethodName: undefined, // AsyncAPI v2 doesn't support x-fern-sdk-method-name on channels
baseUrl,
path,
auth: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class ChannelConverter3_0 extends AbstractChannelConverter<AsyncAPIV3.Cha
channel: {
name: this.context.casingsGenerator.generateName(displayName),
displayName,
connectMethodName: undefined, // This will be populated from OpenAPI IR layer
baseUrl,
path,
auth: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ export function parseAsyncAPIV2({
name: server.name as string
})),
summary: getExtension<string | undefined>(channel, FernAsyncAPIExtension.FERN_DISPLAY_NAME),
connectMethodName: getExtension<string>(channel, FernAsyncAPIExtension.FERN_SDK_METHOD_NAME),
path,
description: channel.description,
examples,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@ export function parseAsyncAPIV3({
FernAsyncAPIExtension.FERN_SDK_GROUP_NAME
);

// Extract connect method name from x-fern-sdk-method-name extension
const connectMethodName = getExtension<string>(channel, FernAsyncAPIExtension.FERN_SDK_METHOD_NAME);

const channelServers = (
channel.servers?.map((serverRef) => getServerNameFromServerRef(servers, serverRef)) ??
Object.values(servers)
Expand Down Expand Up @@ -435,6 +438,7 @@ export function parseAsyncAPIV3({
),
messages,
summary: getExtension<string | undefined>(channel, FernAsyncAPIExtension.FERN_DISPLAY_NAME),
connectMethodName,
servers: channelServers,
path: parsedChannelPath,
description: channel.description,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
}
}
],
"connectMethodName": "createChatConnection",
"servers": [
{
"name": "production",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,62 @@
"file": "../openapi.yml",
"type": "openapi"
}
},
{
"audiences": [],
"sdkName": {
"groupName": [
"webhooks"
],
"methodName": "refundProcessed"
},
"method": "POST",
"operationId": "refundProcessed",
"tags": [],
"headers": [],
"generatedPayloadName": "RefundProcessedWebhooksPayload",
"payload": {
"generatedName": "RefundProcessedWebhooksPayload",
"schema": "RefundEvent",
"source": {
"file": "../openapi.yml",
"type": "openapi"
},
"type": "reference"
},
"signatureVerification": {
"header": "x-refund-signature",
"asymmetricAlgorithm": "ecdsa-sha256",
"encoding": "hex",
"type": "asymmetric"
},
"examples": [
{
"payload": {
"properties": {
"refundId": {
"value": {
"value": "refundId",
"type": "string"
},
"type": "primitive"
},
"amount": {
"value": {
"value": 1.1,
"type": "double"
},
"type": "primitive"
}
},
"type": "object"
}
}
],
"source": {
"file": "../openapi.yml",
"type": "openapi"
}
}
],
"channels": {},
Expand Down Expand Up @@ -355,6 +411,67 @@
"type": "openapi"
},
"type": "object"
},
"RefundEvent": {
"allOf": [],
"properties": [
{
"conflict": {},
"generatedName": "refundEventRefundId",
"key": "refundId",
"schema": {
"schema": {
"type": "string"
},
"generatedName": "RefundEventRefundId",
"groupName": [],
"type": "primitive"
},
"audiences": []
},
{
"conflict": {},
"generatedName": "refundEventAmount",
"key": "amount",
"schema": {
"schema": {
"type": "double"
},
"generatedName": "RefundEventAmount",
"groupName": [],
"type": "primitive"
},
"audiences": []
},
{
"conflict": {},
"generatedName": "refundEventReason",
"key": "reason",
"schema": {
"generatedName": "RefundEventReason",
"value": {
"schema": {
"type": "string"
},
"generatedName": "RefundEventReason",
"groupName": [],
"type": "primitive"
},
"groupName": [],
"type": "optional"
},
"audiences": []
}
],
"allOfPropertyConflicts": [],
"generatedName": "RefundEvent",
"groupName": [],
"additionalProperties": false,
"source": {
"file": "../openapi.yml",
"type": "openapi"
},
"type": "object"
}
},
"namespacedSchemas": {}
Expand Down
Loading
Loading