Custom code
Custom code allows changes anywhere in generated SDKs. Speakeasy automatically preserves those changes across regenerations. Unlike custom code regions, which require predefined areas for customization, custom code provides complete flexibility to modify any generated file.
How it works
Speakeasy preserves manual edits by performing a 3-way merge during generation. This process combines the pristine generated code from the previous generation, the current code on disk, and the newly generated code. If manual changes and generated updates touch the same lines, standard Git-style conflict markers appear for resolution in the editor.
Requirements
Custom code requires a Git repository. The feature uses Git under the hood to track and merge changes, but direct Git interaction is not necessary.
Enabling custom code
For new SDKs
Add the configuration to gen.yaml:
configVersion: 2.0.0
generation:
sdkClassName: MySDK
# ... other configuration
persistentEdits: # Enables custom code preservation
enabled: trueFor existing SDKs
When modifying a generated file and running Speakeasy, a prompt appears showing a diff of the changes:
┃ Changes detected in generated SDK files
┃ The following changes were detected in generated SDK files:
┃ M package.json (+2/-1)
┃ --- generated
┃ +++ current
┃ @@ -22,7 +22,8 @@
┃ "scripts": {
┃ "lint": "eslint --cache --max-warnings=0 src",
┃ "build": "tshy",
┃ - "prepublishOnly": "npm run build"
┃ + "prepublishOnly": "npm run build",
┃ + "test:integration": "node ./scripts/integration.js"
┃ },
┃ ... (2 more lines)
┃
┃ Would you like to enable custom code preservation?
┃ Yes - Enable custom code
┃ > No - Continue without preserving changes
┃ Don't ask againSelect “Yes - Enable custom code” to preserve changes. Speakeasy updates gen.yaml automatically.
When to use custom code
Custom code is appropriate when:
- Adding utility methods or business logic to generated models
- Customizing SDK initialization or configuration
- Integrating with internal systems or libraries
- Making changes that do not fit into predefined extension points
For simpler customizations, consider:
- SDK hooks for lifecycle customization
- Custom code regions for predefined extension areas (Enterprise only)
- OpenAPI extensions for generation-time customization
Common use cases
Adding utility methods
Add helper methods directly to generated model classes:
export class Payment {
id: string;
amount: number;
currency: string;
status: PaymentStatus;
// Custom utility method
toInvoiceItem(): InvoiceItem {
return {
description: `Payment ${this.id}`,
amount: this.amount,
currency: this.currency,
};
}
needsAction(): boolean {
return this.status === PaymentStatus.RequiresAction ||
this.status === PaymentStatus.RequiresConfirmation;
}
}Modifying configuration files
Add custom dependencies or scripts to package configuration:
Extending SDK initialization
Add custom authentication providers or configuration:
import { AWSAuth } from "./custom/aws-auth";
import { MetricsCollector } from "./custom/metrics";
export class MySDK {
private client: HTTPClient;
private awsAuth?: AWSAuth;
private metrics?: MetricsCollector;
constructor(config: SDKConfig) {
this.client = new HTTPClient(config);
// Custom initialization logic
if (config.awsAuth) {
this.awsAuth = new AWSAuth(config.awsAuth);
this.client.interceptors.request.use(
this.awsAuth.signRequest.bind(this.awsAuth)
);
}
if (config.enableMetrics) {
this.metrics = new MetricsCollector(config.metricsEndpoint);
this.client.interceptors.response.use(
this.metrics.recordResponse.bind(this.metrics)
);
}
}
}Handling conflicts
When manual changes and Speakeasy updates modify the same lines, Speakeasy detects the conflict, adds conflict markers to the file, and stages the conflict in the Git index. This ensures that IDEs, git status, and other Git tools recognize the conflict and prompt for resolution:
Merge conflicts detected
1 file(s) have conflicts that must be resolved manually:
both modified: package.json
To resolve:
1. Open each file and resolve the conflict markers (<<<<<<, ======, >>>>>>)
2. Remove the conflict markers after choosing the correct code
3. Run: speakeasy run --skip-versioningFor example, when the SDK version uses a customized prerelease tag and Speakeasy bumps the version during generation:
{
"name": "petstore",
<<<<<<< Current (local changes)
"version": "0.0.2-prerelease",
=======
"version": "0.0.3",
>>>>>>> New (Generated by Speakeasy)
"dependencies": {
...
}
}To resolve conflicts:
- Edit the file to keep the desired code (in this case, decide on the correct version)
- Remove the conflict markers (
<<<<<<<,=======,>>>>>>>) - Run
speakeasy run --skip-versioningto finalize the merge and update internal tracking to accept the resolution - Commit the resolved changes
Working in CI/CD
Custom code works seamlessly in CI/CD environments. The feature runs non-interactively in CI:
- No prompts appear
- Conflicts cause the generation to fail with a clear error message
- The CI job exits with a non-zero code if conflicts occur
The Speakeasy GitHub Action automatically rolls back to the last working generator version if conflicts or other issues occur. This ensures SDK generation remains stable while conflicts are resolved.
Handling conflicts in CI
If conflicts occur during CI generation:
- The CI job fails with an error message listing conflicted files
- Pull the changes locally
- Run
speakeasy runto reproduce the conflicts - Resolve conflicts manually
- Commit and push the resolution
- CI succeeds on the next run
Configuration options
persistentEdits:
# Enable or disable custom code
enabled: trueDisabling custom code
To disable custom code without losing changes:
persistentEdits:
enabled: falseTo prevent prompts entirely:
persistentEdits:
enabled: neverFrequently asked questions
Editing files in the GitHub web UI
Changes made in the GitHub web UI or any Git-based tool are treated like any other manual change. When the repository is cloned locally and Speakeasy runs in that clone, edits are preserved.
Removing generated headers
Important
Do not remove generated headers (like // @generated-id: abc123) when planning to move files. These headers contain tracking information that helps Speakeasy detect file moves.
If headers are removed:
- Custom code still works for files that stay in the same location
- File move detection does not work - moved files are treated as deleted and recreated
Using custom code with custom code regions
Both features can be used together. Custom code regions provide predefined safe areas for customization, while custom code allows changes anywhere. Choose the approach that best fits the use case.
Git operations performed
See the technical reference for details on Git integration.
Resetting to pristine generated code
To discard all custom changes and get fresh generated code:
- Disable custom code in
gen.yaml - Delete the modified files
- Run
speakeasy runto regenerate fresh files - Re-enable custom code if desired
For resetting a single file while keeping other customizations, see the best practices guide.
Next steps
- Review the technical reference for implementation details
- Check out best practices for team workflows
- Learn about custom code regions for predefined customization areas
Last updated on