Multi-language SDK generation automation: complete guide (September 2026)

16 min read

Code generation is the cheapest part of shipping client libraries in nine languages. A generator that turns an OpenAPI document into TypeScript, Python, Go, Java, C#, PHP, Ruby, Swift, and Rust is a solved problem with several credible implementations, including free ones. What is not solved by default is everything downstream: nine repositories to keep current, nine version numbers to reconcile against one API change, nine registries with nine different authentication models, and nine test suites that have to pass before any of it ships. The thesis of this guide is that multi-language SDK generation automation is a release-pipeline problem, not a codegen problem — the tool worth choosing is the one that automates the path from a merged specification change to published packages, because that path is where teams that generate their SDKs still end up doing manual work.

TLDR:

  • Generation is one stage of seven. Validation, custom-code preservation, testing, version computation, publishing, and documentation all have to run unattended for the automation to hold.
  • The per-language work that does not disappear is publishing. npm, PyPI, Maven Central, NuGet, RubyGems, Packagist, crates.io, Go modules, and Swift Package Manager each have a distinct auth model and release trigger.
  • "Idiomatic" is a per-language commitment: Pydantic models in Python, functional options in Go, branded types in TypeScript. A single template engine emitting nine languages tends to produce nine dialects of the same language.
  • Custom code is the reason most SDK automation degrades into manual maintenance. Without file-level or line-level preservation, the first hand-edit forks the repository.
  • Generated unit tests plus mock-server wire tests are the quality gate that makes unattended publishing defensible.
  • Fern generates SDKs in nine languages from one API definition, runs generated unit and wire tests in CI, computes version bumps from the specification diff, and publishes to each language's registry.

What multi-language SDK generation automation actually automates

The phrase describes a continuous pipeline, not a command. A specification change lands on the main branch, and without further human input the system produces validated, tested, versioned packages on every registry a customer might install from. Generation sits in the middle of that pipeline and occupies a small fraction of its complexity.

The stages, in the order they run:

  • Specification validation. Lint the API definition before it reaches a generator. Naming collisions, missing operationId values, and untagged endpoints produce compiling but unusable SDKs.
  • Generation. Turn the definition into source for each target language.
  • Custom-code reconciliation. Reapply hand-written extensions that live in the generated repositories.
  • Testing. Run unit and wire tests per language before anything is published.
  • Version computation. Classify the change and derive the next version number per package.
  • Publishing. Authenticate to each registry and release.
  • Documentation and snippet refresh. Regenerate reference docs and per-language code samples so they match the packages that just shipped.

Teams evaluating SDK generation tools usually compare the generation stage and inherit the other six as engineering work. That inheritance is the real cost, and it is the part that scales with the number of target languages.

Why hand-written client libraries stop scaling

The arithmetic is unforgiving. An API with 120 endpoints across nine languages is 1,080 method implementations, each with its own request model, response model, error mapping, and test. Every new endpoint multiplies by nine. Every field rename multiplies by nine. The work is mechanical, which is precisely why it gets deprioritized against product work, and deprioritized SDK work becomes drift.

Drift in a client library is worse than drift in documentation because it is silent and typed. A stale SDK does not warn a developer that a field was renamed six weeks ago; it deserializes into a model missing that field and returns undefined in production. The failure surfaces in the consumer's application, gets reported as an API bug, and consumes support time from the team that shipped the SDK. Keeping clients evergreen is a property of the pipeline, not of anyone's discipline.

Hand-writing also produces a coverage asymmetry. Teams build the TypeScript and Python clients first because that is where demand is loudest, then let Java, Ruby, and PHP lag by several releases, so developers in the lagging ecosystems fall back to raw HTTP. The API's usability ends up varying by language for reasons that have nothing to do with the API.

Wiring the pipeline into CI

Validate the definition before generating

The generator is not the right place to discover that two parameters normalize to the same identifier. Lint the specification as a required status check: Spectral for style and governance rules, plus whatever generator-specific validation the toolchain offers. Fern's fern check includes a no-conflicting-parameter-names rule that catches header and query parameters collapsing to the same camelCase identifier, which would otherwise ship as a Python SyntaxError and a TypeScript duplicate-property error.

Generate every target from one definition

Generation should be declarative and reproducible. In Fern, the targets live in generators.yml as named groups, each pinning an exact generator version so a rerun of an old commit produces the same code:

groups:
  typescript-sdk:
    generators:
      - name: fernapi/fern-typescript-sdk
        version: 0.9.0
  python-sdk:
    generators:
      - name: fernapi/fern-python-sdk
        version: 2.0.0
aliases:
  all: ["typescript-sdk", "python-sdk"]

Version pinning matters more than it sounds. An unpinned generator turns an unrelated CI run into an unreviewed change across every SDK repository.

Fan out, then converge

Run each language target as its own job so nine toolchains build in parallel rather than in series, then decide explicitly whether a failure in one blocks the rest. Blocking everything keeps the nine packages at a consistent version at the cost of holding a good TypeScript release behind a Rust compile error; releasing independently is faster and produces version skew that support has to explain. Either way, three conditions gate the publish step: generated tests pass for that package, custom-code reconciliation produced no unresolved conflicts, and the version classification is not a major without human approval. Classify that version from a diff of the API definition against the state the last packages were published from, not from commit messages — bot-generated SDK repositories contain no authored commit intent for semantic-release or release-please to parse.

Regenerate documentation in the same run

Reference documentation and per-endpoint code samples should come out of the same definition in the same pipeline execution. Splitting them onto a separate cadence reintroduces the drift the pipeline exists to eliminate, which is the same argument that makes docs-as-code workflows worth adopting in the first place. Snippets are the acute case: a code sample showing a parameter that was renamed two releases ago is worse than no sample, because a developer will paste it.

What "idiomatic" means when one definition targets nine languages

A generated client is idiomatic when a developer reading it cannot tell it was generated, and when their linter and type checker agree with it. That standard is per-language and cannot be satisfied by one template emitting nine outputs with the syntax swapped.

LanguageWhat idiomatic requires
TypeScriptDiscriminated unions on type fields, exported interfaces rather than classes for models, native fetch, ESM and CommonJS builds
PythonPydantic models, type hints throughout, snake_case, async and sync clients
GoFunctional options for configuration, explicit error returns, context.Context as the first parameter, no panics
JavaBuilder patterns, checked exception hierarchies, Maven-friendly package layout
C#async/await with CancellationToken, nullable reference types, IHttpClientFactory compatibility
RubyKeyword arguments, snake_case, blocks for iteration over paginated results
RustResult<T, E> returns, ownership-aware request builders, no unwrapping in library code

The tell for a generator that is not doing this work is a Go client that returns a nullable pointer instead of an error, or a Python client with camelCase method names. Both compile. Neither survives code review at the consuming company, which is the actual acceptance test for a client library.

Beyond naming, the runtime behavior has to be present in every language rather than in the flagship two. Retries with exponential backoff, pagination iterators, configurable timeouts, and idempotency-key support are what separate a client library from a typed HTTP wrapper. Fern generates retry handling into every language with jitter applied to the backoff, retrying on 408, 429, and 5XX, with a default limit of two retries that can be overridden at the client or per-request level (maxRetries in TypeScript, C#, PHP, and Swift; max_retries in Python). It supports cursor, offset, page-number, URI, path, and token-based pagination through the x-fern-pagination extension, so an API with heterogeneous pagination across endpoints still exposes one iteration interface.

Publishing is where multi-language automation actually breaks

Nine languages means nine distribution models, and they have almost nothing in common beyond the word "publish." This is the stage teams most often leave manual, and manual publishing is what makes SDK releases lag API releases by weeks. It is also the stage where automated package publishing tooling earns its cost.

EcosystemDistribution mechanismAuthentication in CI
TypeScriptnpm registryOIDC trusted publishing, or an NPM_TOKEN secret
PythonPyPITrusted Publishers (OIDC), or an API token
JavaMaven Central via the Central Publisher PortalPortal credentials plus a GPG-signed artifact
C#NuGetOIDC or an API key
RubyRubyGemsAPI key, with trusted publishing available
PHPPackagistWebhook from the source repository
Rustcrates.ioAPI token
GoNo registry; the module proxy resolves from a Git tagRepository write access only
SwiftNo registry; Swift Package Manager resolves from a Git tagRepository write access only

Two structural differences matter. Go and Swift have no upload step at all, so "publishing" means pushing a correctly formatted semantic version tag to a repository whose path matches the module declaration — which in turn means the SDK for those languages usually needs its own repository rather than a subdirectory. Java is the strictest, because Maven Central has required PGP signatures since the early 2010s, and Sonatype began validating Sigstore signature bundles on the Central Publisher Portal in January 2025 as an additional provenance layer on top of the long-standing GPG requirement.

The credential model is the other thing that has changed recently and is worth building toward. npm trusted publishing with OIDC went generally available in July 2025, following PyPI's Trusted Publishers. Both exchange a short-lived, workflow-scoped identity token for publish rights, which removes long-lived registry tokens from CI secrets entirely. For an automated multi-language pipeline this is a meaningful reduction in blast radius: a compromised CI secret in the token model publishes arbitrary packages under the organization's name. Fern's output block takes token: OIDC for npm and PyPI, and also supports publishing to private registries such as Artifactory for organizations that do not distribute through public package managers.

output:
  location: npm
  package-name: your-package-name
  token: OIDC

Versioning nine packages against one API change

Two policies are workable and both are defensible. Lockstep versioning gives every language the same number, which keeps support conversations simple at the cost of shipping empty releases to languages a change did not touch. Independent versioning keeps each number accurate and requires the documentation to track several release trajectories at once. Most multi-language programs settle on lockstep majors with independent minors and patches, because the major digit is the one consumers pin against.

Whichever policy applies, the bump should be computed from the contract, because a breaking change in an SDK and a breaking change in an API are not the same set. Adding an enum value is additive over HTTP and a compile-time break in any client that deserializes into a closed enum. Renaming a generated method through generator configuration is a major SDK change with no wire change at all. A pipeline that classifies from the specification diff catches the first; a pipeline that also diffs the generated surface catches the second.

Gating majors on human approval is the one place a person belongs in the loop. Everything else — patch and minor releases, changelog entries, tags — should publish on green.

Preserving custom code across regenerations

Every SDK program eventually needs code the generator will not produce: a convenience wrapper over a three-call sequence, a custom credential provider, a polyfill for a runtime the generator does not target. How the toolchain handles that code determines whether automation survives contact with reality.

Two mechanisms are worth understanding, and they solve different problems:

  • File-level ownership. A .fernignore file lists paths the generator stops touching entirely. Simple and absolute, with a real cost: those files also stop receiving generator improvements. In Fern, .fernignore now also prevents new files from being created at those paths, not just modification and deletion of existing ones.
  • Line-level preservation. Fern's Replay feature keeps hand-edits inside otherwise-generated files by scanning repository history since the last [fern-generated] commit, storing each customer commit as a tracked patch, and reapplying those patches during regeneration via a three-way merge. Conflicts surface as a pull request, or can be worked through locally with fern replay resolve. It is enabled with replay: { enabled: true } in generators.yml.

The failure mode without either mechanism is well known: someone edits a generated file, the next regeneration overwrites it, the team stops regenerating, and the "automated" SDK becomes a hand-maintained one with a generated ancestry. See custom code in Fern SDKs for the per-language conventions.

Testing generated SDKs before they reach a registry

Unattended publishing is only defensible with a quality gate that runs per language. Fern generates two layers automatically and wires them into a GitHub workflow in each SDK repository that runs on every pull request, commit, and release:

  • Unit tests verify individual methods in isolation without network calls, across all supported languages.
  • Mock server (wire) tests stand up a simulated API server and assert that the SDK sends the HTTP requests and parses the HTTP responses the definition describes. They are generated for every endpoint in a service and are available for TypeScript, Python, Go, Java, C#, PHP, Swift, Rust, and Ruby, enabled by default in TypeScript, Go, C#, and Swift.

Wire tests are the layer that catches generator regressions specifically, because they assert on serialized bytes rather than on the shape of an object. Beyond them, handwritten integration tests against a real staging or production API cover the case where the specification and the implementation disagree — the one class of bug that spec-derived tests structurally cannot find. Fern supports adding those tests to SDK repositories and protecting them from regeneration.

The tooling landscape in September 2026

ToolLanguagesModelNotable constraint
OpenAPI Generator50+ targetsOpen source, Mustache templates, self-operatedBreadth over idiomaticity; the pipeline around it is the user's to build
KiotaC#, Go, Java, PHP, Python, Ruby, TypeScript, CLIOpen source, MicrosoftFluent request-builder style rather than language-native idioms
Speakeasy10 (TypeScript, Python, Go, Java, C#, PHP, Ruby, Kotlin, Unity, Terraform)CommercialTypeScript SDKs ship a Zod runtime dependency, which adds bundle weight in browser targets
StainlessTypeScript, Python, Go, Java, Kotlin, Ruby, C#, PHP, TerraformCommercialDocumentation capabilities are newer than the SDK product
Fern9 (TypeScript, Python, Go, Java, C#, PHP, Ruby, Swift, Rust)Commercial, open-source generatorsGenerally available gRPC SDK generation is currently C# only

The split that matters is not the language count. Open-source generators solve the emission step and leave the surrounding pipeline as work; commercial platforms sell the pipeline. A team with an in-house release-engineering function can build around OpenAPI Generator and get a good outcome; a team without one buys a generator and discovers it owns nine release processes. Choosing between open source and commercial generators comes down to whether that pipeline is work the team wants to own.

Challenges that show up after the first generated release

  • Specification quality becomes load-bearing. Missing operationId values produce method names derived from paths, and an untagged endpoint lands in a root namespace. The specification stops being documentation and becomes source code, which usually means it needs review standards it did not previously have.
  • Language expertise does not disappear, it concentrates. Someone still has to evaluate whether the Rust client is idiomatic. Generation removes the volume of that work, not the requirement for judgment.
  • Consumers upgrade on their own schedule. Publishing quickly does not make adoption quick, and branch protection, CODEOWNERS, secret rotation, and Dependabot configuration all multiply by nine whether or not the code inside is generated. Deprecation headers and a documented support window matter more once release velocity increases.
  • Multi-version APIs need a distribution decision. Concurrent API versions can be handled either as separate packages or as one client with a version selector. Fern supports the latter, with version selection controlled through headers rather than separate packages, which avoids maintaining parallel release trains per language.

How Fern automates multi-language SDK generation

Fern generates idiomatic, type-safe SDKs in nine languages — TypeScript, Python, Go, Java, C#, PHP, Ruby, Swift, and Rust — from a single API definition in OpenAPI, AsyncAPI, OpenRPC, gRPC/protobuf, or the Fern Definition format. Targets are declared as groups in generators.yml with pinned generator versions, generated unit and mock-server tests run in CI on every pull request and release, custom code survives regeneration through .fernignore and Replay, and publishing runs to npm, PyPI, Maven Central, NuGet, RubyGems, Packagist, and crates.io with OIDC trusted publishing where the registry supports it. The API definition can be exported back to standard OpenAPI with fern export, so adopting the pipeline does not mean adopting a proprietary source of truth.

Final thoughts on automating multi-language SDK generation

The decision worth getting right is not which generator produces the nicest Go client. It is whether the tool automates the whole path from a merged specification change to published, tested packages on nine registries, or only the code-emission step in the middle. Generators that stop at emission hand back six stages of work that scale with the language count, and that work is where SDK programs quietly become manual again. Evaluate on the pipeline: how versions are computed, how custom code survives, what runs before a package is published, and how each registry is authenticated.

For a walkthrough of that pipeline against a real API definition, book a demo.

FAQ

What is multi-language SDK generation automation?

It is the practice of deriving client libraries for several programming languages from a single API definition and running the full release path — validation, generation, testing, version computation, and publishing — without manual steps per language. The generation step is the visible part; the automation value comes from the stages around it, because those are what scale with the number of target languages.

How many languages should an API provide SDKs for?

Coverage should follow the consumer base rather than a target number. Most public APIs start with TypeScript and Python because they cover web and data workloads, add Go and Java for infrastructure and enterprise consumers, then extend to C#, PHP, Ruby, Swift, and Rust as demand appears. Generation changes the calculus: once the pipeline exists, an additional language is a configuration entry rather than a headcount decision.

Can OpenAPI Generator automate multi-language SDK publishing?

OpenAPI Generator handles code emission for more than 50 targets and is a reasonable foundation, but it does not ship version computation, custom-code preservation, generated test suites, or per-registry publishing. Teams using it build those stages themselves in CI, usually as a per-language workflow plus a shared orchestration script. That is sustainable with a dedicated release-engineering function and expensive without one.

How do generated SDKs get published to npm and PyPI automatically?

Both registries support OIDC-based trusted publishing, where a CI workflow exchanges a short-lived, workflow-scoped identity token for publish rights instead of storing a long-lived token. npm's implementation reached general availability in July 2025 and PyPI's Trusted Publishers predates it. In Fern, this is configured in the output block of generators.yml with token: OIDC, and the publish step runs after the generated tests pass.

How is custom code preserved when SDKs are regenerated?

Two approaches. File-level exclusion through .fernignore gives full ownership of listed paths and stops generator updates to them. Line-level preservation, which Fern implements as Replay, tracks hand-edits as patches and reapplies them across regenerations with a three-way merge, opening a pull request when a conflict cannot be resolved automatically. Without one of the two, the first hand-edit to a generated file effectively forks the SDK.

Should every language SDK share the same version number?

Lockstep versioning is simpler to support and communicate but publishes releases to languages an API change did not affect. Independent versioning keeps each package's number accurate and requires per-language release notes. A common compromise is lockstep major versions with independent minors and patches, since the major digit is what consumers pin and reason about when planning upgrades.