Speakeasy Logo
Skip to Content

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.

Enabling custom code

For new SDKs

Add the configuration to gen.yaml:

.speakeasy/gen.yaml
configVersion: 2.0.0 generation: sdkClassName: MySDK # ... other configuration persistentEdits: # Enables custom code preservation enabled: true

For 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 again

Select “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-versioning

For example, when the SDK version uses a customized prerelease tag and Speakeasy bumps the version during generation:

package.json
{ "name": "petstore", <<<<<<< Current (local changes) "version": "0.0.2-prerelease", ======= "version": "0.0.3", >>>>>>> New (Generated by Speakeasy) "dependencies": { ... } }

To resolve conflicts:

  1. Edit the file to keep the desired code (in this case, decide on the correct version)
  2. Remove the conflict markers (<<<<<<<, =======, >>>>>>>)
  3. Run speakeasy run --skip-versioning to finalize the merge and update internal tracking to accept the resolution
  4. 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 run to reproduce the conflicts
  • Resolve conflicts manually
  • Commit and push the resolution
  • CI succeeds on the next run

Configuration options

.speakeasy/gen.yaml
persistentEdits: # Enable or disable custom code enabled: true

Disabling custom code

To disable custom code without losing changes:

.speakeasy/gen.yaml
persistentEdits: enabled: false

To prevent prompts entirely:

.speakeasy/gen.yaml
persistentEdits: enabled: never

Frequently 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

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:

  1. Disable custom code in gen.yaml
  2. Delete the modified files
  3. Run speakeasy run to regenerate fresh files
  4. Re-enable custom code if desired

For resetting a single file while keeping other customizations, see the best practices guide.

Next steps

Last updated on