Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
873ba9e
created serialization pipeline class
thebarndog Dec 5, 2025
6aab21a
added serialization format to express config
thebarndog Dec 5, 2025
32a26ff
added serialization format to the sdk custom config
thebarndog Dec 5, 2025
af57717
index export
thebarndog Dec 5, 2025
17b6cc4
added serialization formation
thebarndog Dec 5, 2025
afff2c0
integrated initial pipelne into the clis
thebarndog Dec 5, 2025
1d73f02
exports
thebarndog Dec 5, 2025
1dea860
added concrete format implementations
thebarndog Dec 5, 2025
384a716
added tests with benchmarks
thebarndog Dec 5, 2025
6eb3cbd
added to cli class interfaces
thebarndog Dec 5, 2025
8b5426e
removed zurg option
thebarndog Dec 5, 2025
29c5b87
integrated into core utilities manager
thebarndog Dec 5, 2025
0c96516
added options to generators
thebarndog Dec 5, 2025
0a0c447
even more tests
thebarndog Dec 5, 2025
863e804
added backwards compatible interface and removed old zurg code
thebarndog Dec 5, 2025
04a04a2
fixed for biome pr job
thebarndog Dec 5, 2025
42d254b
fixed sorted import
thebarndog Dec 5, 2025
7016a33
fixed export
thebarndog Dec 5, 2025
8a6c730
removed old zurg tests
thebarndog Dec 5, 2025
c6e145d
config
thebarndog Dec 5, 2025
9ac3737
exported shared config
thebarndog Dec 5, 2025
0af9c02
added comments
thebarndog Dec 5, 2025
3ecd521
remove vertical space
thebarndog Dec 5, 2025
ae1244c
Merge branch 'main' of https://github.com/fern-api/fern into feature/…
thebarndog Dec 5, 2025
20e2350
removed erroneous todo
thebarndog Dec 5, 2025
08106d0
removed unused import
thebarndog Dec 5, 2025
7ea1bb2
changed default back to zurg
thebarndog Dec 5, 2025
4b87b52
fixed remaining default values
thebarndog Dec 5, 2025
8fc0d49
renamed none to passthrough
thebarndog Dec 5, 2025
e483e1d
added to seed yaml
thebarndog Dec 5, 2025
1640453
test generation
thebarndog Dec 5, 2025
61cbaa4
removed vertical line
thebarndog Dec 5, 2025
f83112f
Merge branch 'main' into feature/serialization-pipeline
thebarndog Dec 5, 2025
7b1ba09
added zod imports
thebarndog Dec 5, 2025
885662a
Merge branch 'feature/serialization-pipeline' of https://github.com/f…
thebarndog Dec 5, 2025
fcca365
Merge branch 'main' into feature/serialization-pipeline
thebarndog Dec 5, 2025
72794d0
fixed missing zod import
thebarndog Dec 5, 2025
ba6c345
Merge branch 'feature/serialization-pipeline' of https://github.com/f…
thebarndog Dec 5, 2025
cd96c5e
tweaked ast generation for zod
thebarndog Dec 5, 2025
1922eeb
updates test
thebarndog Dec 5, 2025
7c83728
test updates
thebarndog Dec 5, 2025
132841a
typecast
thebarndog Dec 5, 2025
a6d1b35
using ZodTypeAny type eraser
thebarndog Dec 5, 2025
64d4513
fixed toExpression to transform into an array
thebarndog Dec 5, 2025
9ce20f1
updated tests
thebarndog Dec 5, 2025
0683c16
d
thebarndog Dec 5, 2025
189800c
implemented custom json serialization for zod to fix type mismatches
thebarndog Dec 5, 2025
0930f21
fixed missing parameters
thebarndog Dec 5, 2025
f353175
fixed linter issue
thebarndog Dec 5, 2025
8066cf3
fixed zod serialization and verified tests pass
thebarndog Dec 5, 2025
33257af
added initial recursive implementation, runs at O(n)
thebarndog Dec 5, 2025
75992c8
comments about recursive serialization for zod
thebarndog Dec 5, 2025
f0334a8
fixed final zod tests
thebarndog Dec 6, 2025
eb2592f
refactored zurg to mimic zod format pipeline
thebarndog Dec 6, 2025
75c6f90
fixed zurg test failures
thebarndog Dec 6, 2025
2980b21
generated tests
thebarndog Dec 6, 2025
de56e41
fixed unit tests
thebarndog Dec 6, 2025
5be5be9
fix for express generator
thebarndog Dec 6, 2025
a85e11e
added some doc comments
thebarndog Dec 6, 2025
36af03c
removed exhaustive tests for pr review
thebarndog Dec 6, 2025
6885794
fixed docs
thebarndog Dec 6, 2025
a2140d0
updated type
thebarndog Dec 6, 2025
9f6a5b8
reverted change to config path
thebarndog Dec 6, 2025
4552c68
replaced with type
thebarndog Dec 6, 2025
b89c020
Reverted some changes to zurg format to keep generated code consistent
thebarndog Dec 6, 2025
b3d429a
generated seed files
thebarndog Dec 6, 2025
f186607
used version for zod
thebarndog Dec 6, 2025
d5138b2
code organization
thebarndog Dec 6, 2025
d5363a6
comments
thebarndog Dec 6, 2025
3a508a5
fixed comment
thebarndog Dec 6, 2025
a0e84b1
final tests
thebarndog Dec 6, 2025
2494bcc
linting
thebarndog Dec 6, 2025
e525563
fixed missing namespace in tests
thebarndog Dec 6, 2025
725c404
Merge branch 'main' into feature/serialization-pipeline
thebarndog Dec 6, 2025
33dbc40
fixed express error
thebarndog Dec 6, 2025
653ae92
removed generated seed tests for pr review
thebarndog Dec 6, 2025
04b2aed
seed
Swimburger Dec 8, 2025
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
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const TypescriptCustomConfigSchema = z.strictObject({
inlinePathParameters: z.optional(z.boolean()),
namespaceExport: z.optional(z.string()),
noSerdeLayer: z.optional(z.boolean()),
serializationFormat: z.optional(z.enum(["zurg", "zod", "none"])),
private: z.optional(z.boolean()),
requireDefaultEnvironment: z.optional(z.boolean()),
retainOriginalCasing: z.optional(z.boolean()),
Expand Down
13 changes: 11 additions & 2 deletions generators/typescript/express/cli/src/ExpressGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Logger } from "@fern-api/logger";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import { IntermediateRepresentation } from "@fern-fern/ir-sdk/api";
import { AbstractGeneratorCli } from "@fern-typescript/abstract-generator-cli";
import { NpmPackage, PersistedTypescriptProject } from "@fern-typescript/commons";
import { NpmPackage, PersistedTypescriptProject, SerializationPipeline } from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
import { ExpressGenerator } from "@fern-typescript/express-generator";
import { camelCase, upperFirst } from "lodash-es";
Expand All @@ -12,7 +12,14 @@ import { ExpressCustomConfigSchema } from "./custom-config/schema/ExpressCustomC
export class ExpressGeneratorCli extends AbstractGeneratorCli<ExpressCustomConfig> {
protected parseCustomConfig(customConfig: unknown, logger: Logger): ExpressCustomConfig {
const parsed = customConfig != null ? ExpressCustomConfigSchema.parse(customConfig) : undefined;
const noSerdeLayer = parsed?.noSerdeLayer ?? false;

// Resolve serialization format from new option or legacy noSerdeLayer
const serializationFormat = SerializationPipeline.resolveFormatType({
serializationFormat: parsed?.serializationFormat,
noSerdeLayer: parsed?.noSerdeLayer
});
const noSerdeLayer = serializationFormat === "none";

const enableInlineTypes = false; // hardcode, not supported in Express
const config = {
useBrandedStringAliases: parsed?.useBrandedStringAliases ?? false,
Expand All @@ -22,6 +29,7 @@ export class ExpressGeneratorCli extends AbstractGeneratorCli<ExpressCustomConfi
includeOtherInUnionTypes: parsed?.includeOtherInUnionTypes ?? false,
treatUnknownAsAny: parsed?.treatUnknownAsAny ?? false,
noSerdeLayer,
serializationFormat,
requestValidationStatusCode: parsed?.requestValidationStatusCode ?? 422,
outputEsm: parsed?.outputEsm ?? false,
outputSourceFiles: parsed?.outputSourceFiles ?? true,
Expand Down Expand Up @@ -76,6 +84,7 @@ export class ExpressGeneratorCli extends AbstractGeneratorCli<ExpressCustomConfi
includeOtherInUnionTypes: customConfig.includeOtherInUnionTypes,
treatUnknownAsAny: customConfig.treatUnknownAsAny,
includeSerdeLayer: !customConfig.noSerdeLayer,
serializationFormat: customConfig.serializationFormat,
outputEsm: customConfig.outputEsm,
retainOriginalCasing: customConfig.retainOriginalCasing,
allowExtraFields: customConfig.allowExtraFields,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SerializationFormatType } from "@fern-typescript/commons";

// this is the parsed config shape. to view the allowed options for generators.yml,
// see ExpressCustomConfigSchema.ts
export interface ExpressCustomConfig {
Expand All @@ -8,6 +10,7 @@ export interface ExpressCustomConfig {
includeOtherInUnionTypes: boolean;
treatUnknownAsAny: boolean;
noSerdeLayer: boolean;
serializationFormat: SerializationFormatType;
skipRequestValidation: boolean;
skipResponseValidation: boolean;
requestValidationStatusCode: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { z } from "zod";

/**
* Serialization format options:
* - "zurg": Use Zurg (bundled runtime) - same as legacy behavior
* - "zod": Use Zod as npm dependency
* - "none": No serialization layer - same as noSerdeLayer: true
*/
const SerializationFormatSchema = z.enum(["zurg", "zod", "none"]);

export const ExpressCustomConfigSchema = z.strictObject({
useBrandedStringAliases: z.optional(z.boolean()),
optionalImplementations: z.optional(z.boolean()),
doNotHandleUnrecognizedErrors: z.optional(z.boolean()),
treatUnknownAsAny: z.optional(z.boolean()),
noSerdeLayer: z.optional(z.boolean()),
serializationFormat: z.optional(SerializationFormatSchema),
skipRequestValidation: z.optional(z.boolean()),
skipResponseValidation: z.optional(z.boolean()),
outputEsm: z.optional(z.boolean()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ImportsManager,
NpmPackage,
PackageId,
SerializationFormatType,
SimpleTypescriptProject,
TypescriptProject
} from "@fern-typescript/commons";
Expand Down Expand Up @@ -59,6 +60,7 @@ export declare namespace ExpressGenerator {
includeOtherInUnionTypes: boolean;
treatUnknownAsAny: boolean;
includeSerdeLayer: boolean;
serializationFormat: SerializationFormatType;
outputEsm: boolean;
retainOriginalCasing: boolean;
allowExtraFields: boolean;
Expand Down Expand Up @@ -134,7 +136,8 @@ export class ExpressGenerator {
fetchSupport: "node-fetch",
relativePackagePath: this.getRelativePackagePath(),
relativeTestPath: this.getRelativeTestPath(),
generateEndpointMetadata: false
generateEndpointMetadata: false,
serializationFormat: config.serializationFormat
});
this.asIsManager = new AsIsManager({
useBigInt: config.useBigInt,
Expand Down Expand Up @@ -525,7 +528,7 @@ export class ExpressGenerator {
this.context.logger.debug(`Generating ${filepathStr}`);

const sourceFile = this.rootDirectory.createSourceFile(filepathStr);
const importsManager = new ImportsManager({ packagePath: this.config.packagePath });
const importsManager = new ImportsManager({ packagePath: this.getRelativePackagePath() });

run({ sourceFile, importsManager });

Expand Down
13 changes: 12 additions & 1 deletion generators/typescript/sdk/cli/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
fixImportsForEsm,
NpmPackage,
PersistedTypescriptProject,
SerializationPipeline,
writeTemplateFiles
} from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
Expand All @@ -35,7 +36,15 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {

protected parseCustomConfig(customConfig: unknown, logger: Logger): SdkCustomConfig {
const parsed = customConfig != null ? SdkCustomConfigSchema.parse(customConfig) : undefined;
const noSerdeLayer = parsed?.noSerdeLayer ?? true;

// Resolve serialization format from new option or legacy noSerdeLayer
// Note: SDK defaults to noSerdeLayer: true (no serialization) for backward compatibility
const serializationFormat = SerializationPipeline.resolveFormatType({
serializationFormat: parsed?.serializationFormat,
noSerdeLayer: parsed?.noSerdeLayer ?? true
});
const noSerdeLayer = serializationFormat === "none";

const config = {
useBrandedStringAliases: parsed?.useBrandedStringAliases ?? false,
outputSourceFiles: parsed?.outputSourceFiles ?? true,
Expand All @@ -58,6 +67,7 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {
treatUnknownAsAny: parsed?.treatUnknownAsAny ?? false,
includeContentHeadersOnFileDownloadResponse: parsed?.includeContentHeadersOnFileDownloadResponse ?? false,
noSerdeLayer,
serializationFormat,
extraPeerDependencies: parsed?.extraPeerDependencies ?? {},
extraPeerDependenciesMeta: parsed?.extraPeerDependenciesMeta ?? {},
noOptionalProperties: parsed?.noOptionalProperties ?? false,
Expand Down Expand Up @@ -207,6 +217,7 @@ export class SdkGeneratorCli extends AbstractGeneratorCli<SdkCustomConfig> {
treatUnknownAsAny: customConfig.treatUnknownAsAny,
includeContentHeadersOnFileDownloadResponse: customConfig.includeContentHeadersOnFileDownloadResponse,
includeSerdeLayer: !customConfig.noSerdeLayer,
serializationFormat: customConfig.serializationFormat,
retainOriginalCasing: customConfig.retainOriginalCasing ?? false,
parameterNaming: customConfig.parameterNaming ?? "default",
noOptionalProperties: customConfig.noOptionalProperties,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SerializationFormatType } from "@fern-typescript/commons";

// this is the parsed config shape. to view the allowed options for generators.yml,
// see SdkCustomConfigSchema.ts
export interface SdkCustomConfig {
Expand All @@ -24,6 +26,7 @@ export interface SdkCustomConfig {
treatUnknownAsAny: boolean;
includeContentHeadersOnFileDownloadResponse: boolean;
noSerdeLayer: boolean;
serializationFormat: SerializationFormatType;
noOptionalProperties: boolean;
includeApiReference: boolean | undefined;
tolerateRepublish: boolean;
Expand Down
5 changes: 4 additions & 1 deletion generators/typescript/sdk/generator/src/SdkGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
NpmPackage,
PackageId,
PublicExportsManager,
SerializationFormatType,
SimpleTypescriptProject,
TypescriptProject
} from "@fern-typescript/commons";
Expand Down Expand Up @@ -124,6 +125,7 @@ export declare namespace SdkGenerator {
treatUnknownAsAny: boolean;
includeContentHeadersOnFileDownloadResponse: boolean;
includeSerdeLayer: boolean;
serializationFormat: SerializationFormatType;
noOptionalProperties: boolean;
tolerateRepublish: boolean;
retainOriginalCasing: boolean;
Expand Down Expand Up @@ -289,7 +291,8 @@ export class SdkGenerator {
fetchSupport: this.config.fetchSupport,
relativePackagePath: this.relativePackagePath,
relativeTestPath: this.relativeTestPath,
generateEndpointMetadata: this.config.generateEndpointMetadata
generateEndpointMetadata: this.config.generateEndpointMetadata,
serializationFormat: this.config.serializationFormat
});

const apiDirectory: ExportedDirectory[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,136 @@ export abstract class AbstractGeneratedSchema<Context extends BaseContext> {
}

public writeSchemaToFile(context: Context): void {
context.sourceFile.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: this.typeName,
type: getTextOfTsNode(
this.getReferenceToSchemaType({
context,
rawShape: this.getReferenceToRawShape(context),
parsedShape: this.getReferenceToParsedShape(context)
})
const schema = this.buildSchema(context);
const schemaExpression = schema.toExpression();
const toJsonExpression = schema.toJsonExpression;

// For Zod format, always generate a wrapper object with parse and json methods
// This is needed because Zod schemas don't have built-in bidirectional serialization
if (context.coreUtilities.zurg.name === "zod") {
// Generate:
// const _<typeName>_Schema = <zodSchema>;
// export const <typeName> = { _schema: _<typeName>_Schema, parse: ..., json: ... };
const internalSchemaName = `_${this.typeName}_Schema`;
const parsedParam = ts.factory.createIdentifier("parsed");
const rawParam = ts.factory.createIdentifier("raw");
const schemaRef = ts.factory.createIdentifier(internalSchemaName);

// First, add the internal schema variable
context.sourceFile.addVariableStatement({
isExported: false,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: internalSchemaName,
initializer: getTextOfTsNode(schemaExpression)
}
]
});

// If no toJsonExpression, use identity function: (parsed) => parsed
const jsonBody = toJsonExpression ? toJsonExpression(parsedParam) : parsedParam;

// Get the type nodes for parameter annotations
const parsedTypeNode = this.getReferenceToParsedShape(context);

// Get the raw type node for json return type cast
const rawTypeNode = this.getReferenceToRawShape(context);

const wrapperObject = ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment("_schema", schemaRef),
ts.factory.createPropertyAssignment(
"parse",
ts.factory.createArrowFunction(
undefined,
undefined,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
undefined,
rawParam,
undefined,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
)
],
undefined,
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
// Cast the parse result to the expected Parsed type to fix Zod's loose inference
ts.factory.createAsExpression(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(schemaRef, "parse"),
undefined,
[rawParam]
),
parsedTypeNode
)
)
),
initializer: getTextOfTsNode(this.buildSchema(context).toExpression())
}
]
});
ts.factory.createPropertyAssignment(
"json",
ts.factory.createArrowFunction(
undefined,
undefined,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
undefined,
parsedParam,
undefined,
parsedTypeNode
)
],
undefined,
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
// Cast the json result to the expected Raw type
ts.factory.createAsExpression(jsonBody, rawTypeNode)
)
)
],
true
);

// Then add the exported wrapper
context.sourceFile.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: this.typeName,
type: getTextOfTsNode(
this.getReferenceToSchemaType({
context,
rawShape: this.getReferenceToRawShape(context),
parsedShape: this.getReferenceToParsedShape(context)
})
),
initializer: getTextOfTsNode(wrapperObject)
}
]
});
} else {
// Original behavior for Zurg format
context.sourceFile.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: this.typeName,
type: getTextOfTsNode(
this.getReferenceToSchemaType({
context,
rawShape: this.getReferenceToRawShape(context),
parsedShape: this.getReferenceToParsedShape(context)
})
),
initializer: getTextOfTsNode(schemaExpression)
}
]
});
}

this.generateModule(context);
}
Expand Down
Loading
Loading