Skip to content
Open
Show file tree
Hide file tree
Changes from 24 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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
* - "default": 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(["default", "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 @@ -59,6 +59,7 @@ export declare namespace ExpressGenerator {
includeOtherInUnionTypes: boolean;
treatUnknownAsAny: boolean;
includeSerdeLayer: boolean;
serializationFormat: "default" | "zod" | "none";
outputEsm: boolean;
retainOriginalCasing: boolean;
allowExtraFields: boolean;
Expand Down Expand Up @@ -134,7 +135,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
18 changes: 17 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,8 @@ import {
fixImportsForEsm,
NpmPackage,
PersistedTypescriptProject,
SerializationFormatType,
SerializationPipeline,
writeTemplateFiles
} from "@fern-typescript/commons";
import { GeneratorContext } from "@fern-typescript/contexts";
Expand All @@ -35,7 +37,19 @@ 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
// TODO: Add serializationFormat to TypescriptCustomConfigSchema in @fern-api/typescript-ast
const parsedWithFormat = parsed as
| (typeof parsed & { serializationFormat?: SerializationFormatType })
| undefined;
const serializationFormat = SerializationPipeline.resolveFormatType({
serializationFormat: parsedWithFormat?.serializationFormat,
noSerdeLayer: parsed?.noSerdeLayer ?? true
});
const noSerdeLayer = serializationFormat === "none";

const config = {
useBrandedStringAliases: parsed?.useBrandedStringAliases ?? false,
outputSourceFiles: parsed?.outputSourceFiles ?? true,
Expand All @@ -58,6 +72,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 +222,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
4 changes: 3 additions & 1 deletion generators/typescript/sdk/generator/src/SdkGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export declare namespace SdkGenerator {
treatUnknownAsAny: boolean;
includeContentHeadersOnFileDownloadResponse: boolean;
includeSerdeLayer: boolean;
serializationFormat: "default" | "zod" | "none";
noOptionalProperties: boolean;
tolerateRepublish: boolean;
retainOriginalCasing: boolean;
Expand Down Expand Up @@ -289,7 +290,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 @@ -8,6 +8,14 @@ import { DependencyManager } from "../dependency-manager/DependencyManager";
import { ExportsManager } from "../exports-manager";
import { ImportsManager } from "../imports-manager";
import { getReferenceToExportViaNamespaceImport } from "../referencing";
import {
NoneFormat,
SerializationFormatType,
SerializationPipeline,
ZodFormat,
ZURG_MANIFEST,
ZurgFormat
} from "../serialization-pipeline";
import { AuthImpl } from "./Auth";
import { CallbackQueueImpl } from "./CallbackQueue";
import { CoreUtilities } from "./CoreUtilities";
Expand All @@ -22,7 +30,6 @@ import { StreamImpl } from "./Stream";
import { UrlUtilsImpl } from "./UrlUtils";
import { UtilsImpl } from "./Utils";
import { WebsocketImpl } from "./Websocket";
import { ZurgImpl } from "./Zurg";

export declare namespace CoreUtilitiesManager {
namespace getCoreUtilities {
Expand Down Expand Up @@ -51,28 +58,32 @@ export class CoreUtilitiesManager {
private readonly relativePackagePath: string;
private readonly relativeTestPath: string;
private readonly generateEndpointMetadata: boolean;
private readonly serializationFormat: SerializationFormatType;

constructor({
streamType,
formDataSupport,
fetchSupport,
relativePackagePath = DEFAULT_PACKAGE_PATH,
relativeTestPath = DEFAULT_TEST_PATH,
generateEndpointMetadata
generateEndpointMetadata,
serializationFormat = "default"
}: {
streamType: "wrapper" | "web";
formDataSupport: "Node16" | "Node18";
fetchSupport: "node-fetch" | "native";
relativePackagePath?: string;
relativeTestPath?: string;
generateEndpointMetadata: boolean;
serializationFormat?: SerializationFormatType;
}) {
this.streamType = streamType;
this.formDataSupport = formDataSupport;
this.fetchSupport = fetchSupport;
this.relativePackagePath = relativePackagePath;
this.relativeTestPath = relativeTestPath;
this.generateEndpointMetadata = generateEndpointMetadata;
this.serializationFormat = serializationFormat;
}

public getCoreUtilities({
Expand All @@ -90,8 +101,11 @@ export class CoreUtilitiesManager {
relativeTestPath
});

// Create the serialization format based on configuration
const serializationFormat = this.createSerializationFormat(getReferenceToExport);

return {
zurg: new ZurgImpl({ getReferenceToExport, generateEndpointMetadata: this.generateEndpointMetadata }),
zurg: serializationFormat,
fetcher: new FetcherImpl({ getReferenceToExport, generateEndpointMetadata: this.generateEndpointMetadata }),
stream: new StreamImpl({ getReferenceToExport, generateEndpointMetadata: this.generateEndpointMetadata }),
auth: new AuthImpl({ getReferenceToExport, generateEndpointMetadata: this.generateEndpointMetadata }),
Expand Down Expand Up @@ -128,6 +142,34 @@ export class CoreUtilitiesManager {
};
}

private createSerializationFormat(
getReferenceToExport: (args: {
manifest: CoreUtility.Manifest;
exportedName: string;
}) => ReturnType<typeof getReferenceToExportViaNamespaceImport>
) {
const config = {
getReferenceToExport,
generateEndpointMetadata: this.generateEndpointMetadata
};

switch (this.serializationFormat) {
case "default":
// Add Zurg manifest to referenced utilities so it gets copied
this.addManifestAndDependencies(ZURG_MANIFEST);
return new ZurgFormat(config);

case "zod":
return new ZodFormat(config);

case "none":
return new NoneFormat(config);

default:
throw new Error(`Unknown serialization format: ${this.serializationFormat}`);
}
}

public finalize(exportsManager: ExportsManager, dependencyManager: DependencyManager): void {
for (const utility of Object.values(this.referencedCoreUtilities)) {
exportsManager.addExportsForDirectories(
Expand All @@ -145,6 +187,20 @@ export class CoreUtilitiesManager {
fetchSupport: this.fetchSupport
});
}

// Add runtime dependencies for serialization format
this.addSerializationDependencies(dependencyManager);
}

/**
* Add npm dependencies required by the active serialization format
*/
private addSerializationDependencies(dependencyManager: DependencyManager): void {
if (this.serializationFormat === "zod") {
// Zod uses an npm dependency instead of bundled runtime files
dependencyManager.addDependency("zod", "^3.23.0");
}
// Zurg and None formats don't require external npm dependencies
}

public async copyCoreUtilities({
Expand Down
Loading
Loading