--- title: Fern vs Stainless: SDK generation comparison | Stainless docs description: A comprehensive comparison of Fern and Stainless SDK generation: features, language support, configuration, documentation, and more. --- Fern and Stainless both generate client SDKs from OpenAPI specifications, aiming to replace the hand-written SDK maintenance burden that slows down API teams. Both tools produce typed, idiomatic libraries across multiple programming languages and offer documentation solutions alongside SDK generation. This comparison provides a factual overview of how the two platforms differ so you can evaluate which one fits your needs. It covers codegen targets, documentation, SDK features, SDK design, developer workflow, runtime dependencies, and MCP server support. ## Why teams choose Stainless - **Battle-tested at scale**: Stainless SDKs are downloaded over 130 million times per week and trusted by API leaders like Anthropic, Cloudflare, Google, and OpenAI. That scale means more edge cases solved and more production-hardened output than any other codegen platform. - **Flexible and customizable**: Add custom code anywhere in your SDKs (persists across regeneration), fully customize your generated docs, and fine-tune naming, design, and deployment to match your needs. - **Complete API platform**: SDKs, docs, CLI tools, Terraform providers, and MCP servers all generate from one OpenAPI spec and one config file, so each additional target you adopt makes the whole platform more valuable. This comparison is based on publicly available documentation and [public GitHub repositories](https://github.com/fern-api) from both platforms as of February 2026. Features and pricing may have changed since then. We have made our best effort to be accurate. If you notice any inaccuracies, please email us at . ## Codegen targets Both Stainless and Fern support SDKs for the most popular programming languages. The key differences lie in additional codegen targets beyond traditional SDKs. Stainless generates more than just language SDKs. Terraform providers, CLI tools, and MCP servers extend how your users interact with your API beyond traditional SDK integrations. | Language | Stainless | Fern | | ------------------------------------------------------ | --------------- | ------- | | [TypeScript](/docs/targets/typescript/index.md) | ✅ | ✅ | | [Python](/docs/targets/python/index.md) | ✅ | ✅ | | [Java](/docs/targets/java/index.md) | ✅ | ✅ | | [Go](/docs/targets/go/index.md) | ✅ | ✅ | | [Kotlin](/docs/targets/kotlin/index.md) | ✅ | ❌ | | [Ruby](/docs/targets/ruby/index.md) | ✅ | ✅ | | [PHP](/docs/targets/php/index.md) | ✅ | ✅ | | [C# (.NET)](/docs/targets/csharp/index.md) | ✅ | ✅ | | Rust | Planned | ✅ | | [Terraform provider](/docs/targets/terraform/index.md) | ✅ | ❌ | | [SQL](/docs/targets/sql/index.md) | ⚠️ Experimental | ❌ | | [CLI tool](/docs/targets/cli/index.md) | ✅ | ❌ | | [MCP server](/docs/mcp/index.md) | ✅ | ❌ | | Postman collection | ❌ | ✅ | | Swift | Planned | ⚠️ Beta | Beyond language SDKs, Stainless generates three additional targets that extend how your users can interact with your API: - **Terraform provider**: Generates a fully functional Terraform provider from your OpenAPI spec, so your users can manage API resources with infrastructure-as-code workflows. - **CLI tool**: Generates a command-line interface for your API, giving users a way to interact with endpoints directly from the terminal. - **MCP server**: Generates a Model Context Protocol server, enabling AI agents to discover and call your API automatically. - **SQL SDK (experimental)**: Generates a PostgreSQL extension that exposes your API as SQL functions, allowing users to query your API directly from their database. ## SDK features The features built into your generated SDKs determine how much boilerplate your API consumers write and how robust their integrations are out of the box. Stainless SDKs ship with sensible defaults like automatic environment variable reading, rich error hierarchies, and forward compatibility, so your users write less boilerplate and build more resilient integrations. | Feature | Stainless | Fern | | --------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------ | | Authentication | ✅ Auto-reads env vars | ✅ Manual configuration | | Retries with backoff | ✅ | ✅ | | Timeouts | ✅ | ✅ | | Auto-pagination | ✅ | ✅ | | Error handling | ✅ Rich error hierarchy | ✅ Basic error types | | Raw responses | ✅ | ✅ | | Forward compatibility | ✅ Extra fields preserved | ⚠️ some languages (e.g. TypeScript ignores unknown, PHP loses them on reserialization, closed enums in Rust) | | Idempotency keys | ✅ Default on POST | ✅ | | Webhooks | ✅ Signature verification | ✅ Signature verification | | SSE / Streaming | ✅ | ✅ | | Automated tests | ✅ Generated per-endpoint | ⚠️ Mock server only | ### Error handling Stainless generates a rich error hierarchy with typed access to error details, while Fern provides basic error types. - [Stainless TypeScript](#tab-panel-25) - [Fern TypeScript](#tab-panel-26) ``` import Example, { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError, BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, UnprocessableEntityError, RateLimitError, InternalServerError, ExampleError, } from "stainless-demo-sdk"; async function createWidget() { try { const widget = await client.widget.create({ name: "Test" }); return widget; } catch (err) { if (err instanceof BadRequestError) { // 400 - invalid request parameters console.error("Bad request:", err.status, err.message); console.error("Error body:", err.error); return; } if (err instanceof AuthenticationError) { // 401 - missing or invalid credentials console.error("Auth failed:", err.message); return; } if (err instanceof PermissionDeniedError) { // 403 - insufficient permissions console.error("Forbidden:", err.message); return; } if (err instanceof NotFoundError) { // 404 - resource not found console.error("Not found:", err.message); return; } if (err instanceof ConflictError) { // 409 - conflicts with current resource state console.error("Conflict:", err.message); return; } if (err instanceof UnprocessableEntityError) { // 422 - validation failed console.error("Validation error:", err.message, err.error); return; } if (err instanceof RateLimitError) { // 429 - rate limited const retryAfter = err.headers?.get("retry-after"); console.error("Rate limited. Retry after:", retryAfter); return; } if (err instanceof InternalServerError) { // 5xx - server error console.error("Server error:", err.status, err.message); return; } // --- Connection errors --- if (err instanceof APIConnectionTimeoutError) { // Request timed out (subclass of APIConnectionError, check first) console.error("Timeout:", err.message); return; } if (err instanceof APIConnectionError) { // Network failure, DNS resolution, etc. console.error("Connection failed:", err.message, err.cause); return; } // --- User abort --- if (err instanceof APIUserAbortError) { // Request cancelled via AbortSignal console.error("Request aborted:", err.message); return; } // --- Catch-all for any other API error --- if (err instanceof APIError) { // Unexpected status code not covered above console.error("API error:", err.status, err.message); console.error("Headers:", err.headers); console.error("Body:", err.error); return; } // --- Base SDK error (non-API) --- if (err instanceof ExampleError) { console.error("SDK error:", err.message); return; } throw err; // Re-throw unknown errors } } ``` ``` import { ExampleClient } from "fern-demo-sdk"; import { ExampleError, ExampleTimeoutError } from "fern-demo-sdk/errors"; import { ContentTooLargeError, NotFoundError, UnprocessableEntityError, } from "fern-demo-sdk/api/errors"; async function createWidget() { try { const widget = await client.widgets.create({ name: "Test" }); return widget; } catch (err) { // --- API-specific errors (generated from spec, most specific first) --- if (err instanceof ContentTooLargeError) { // 413 - body is typed as Example.ErrorResponse console.error("Content too large:", err.statusCode, err.body); return; } if (err instanceof NotFoundError) { // 404 - body is typed as Example.ErrorResponse console.error("Not found:", err.statusCode, err.body); console.error("Response:", err.rawResponse); return; } if (err instanceof UnprocessableEntityError) { // 422 - body is typed as Example.HttpValidationError console.error("Validation error:", err.statusCode, err.body); return; } // --- Timeout error --- if (err instanceof ExampleTimeoutError) { // Request timed out (separate class, not a subclass of ExampleError) console.error("Request timed out:", err.message); return; } // --- Catch-all base API error --- if (err instanceof ExampleError) { // Any other HTTP error not generated as a specific class console.error("API error:", err.statusCode, err.message); console.error("Body:", err.body); console.error("Raw response:", err.rawResponse); return; } throw err; // Re-throw unknown errors } } ``` ### Forward compatibility Stainless SDKs preserve unknown fields from API responses, so your users can access new fields before updating to the latest SDK version. - [Stainless TypeScript](#tab-panel-27) - [Fern TypeScript](#tab-panel-28) ``` // Stainless preserves unknown fields on responses - new API fields are // accessible immediately, before the SDK is updated const user = await client.users.get('123'); console.log(user.newField); // accessible even before SDK update // Extra params are also forwarded on every request method const widget = await client.widgets.create({ name: "Test", // @ts-ignore - forward extra request params to the API newParam: "value", }); ``` ``` // Fern TypeScript silently drops unknown fields from API responses. // New fields added to the API are not accessible until the SDK is updated. const user = await client.users.get('123'); console.log(user.newField); // undefined - unknown field was ignored // TypeScript also won't allow this without a cast: // Property 'newField' does not exist on type 'User' // You can cast to access unknown fields, but lose type safety: const raw = user as Record; console.log(raw.newField); // accessible, but no type guarantee ``` ### Environment variable detection Stainless SDKs read API keys from environment variables, reducing configuration boilerplate. ``` // Stainless - auto-reads from environment const client = new MyStainlessClient(); // reads MY_API_KEY from env // Fern - manual configuration required const client = new MyFernClient({ apiKey: process.env.MY_API_KEY }); ``` ## SDK design The design of a generated SDK directly affects how quickly developers can adopt your API and how confidently they can build on it. Well-designed SDKs reduce friction by providing clear types, idiomatic patterns, and consistent behavior across languages. Stainless invests in clean type consolidation and consistent error patterns across all eight languages, producing SDKs that feel hand-written rather than auto-generated. | Aspect | Stainless | Fern | | -------------------------- | ---------------------------------- | ---------------------------------------- | | Type consolidation | Clean deduplication | Collisions reported in complex specs | | Discriminated unions | ✅ | ✅ | | Consistent error hierarchy | Unified pattern across 8 languages | Varies by language | | Method naming | Idiomatic per language | Mirrors OpenAPI `operationId` by default | | Sync and async clients | ✅ | ✅ | ## Workflow How you configure SDK generation and integrate it into your development workflow affects how quickly you can iterate on your SDKs. A single configuration file and semantic merge for custom code let teams iterate faster without worrying about losing their customizations or managing multiple config files. | Aspect | Stainless | Fern | | ------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------- | | OpenAPI support | Native, OpenAPI-first | OpenAPI or Fern Definition | | [Configuration](/docs/guides/configure/index.md) | Single `stainless.yml` | Multiple files (`generators.yml`, `docs.yml`, etc.) | | [Custom code handling](/docs/guides/add-custom-code/index.md) | Edits persist through regeneration | `.fernignore` excludes files from future generation | | [OpenAPI customization](/docs/guides/transforms/index.md) | Transforms (programmatic, code-level) | `overrides.yml` | | [Local development](/docs/guides/iterate-with-lsp/index.md) | CLI, VS Code Extension, LSP, MCP server | CLI with local generation | | [Preview builds](/docs/guides/preview-builds/index.md) | ✅ GitHub Action to preview SDK changes when OpenAPI spec updates | ❌ | Stainless uses a single `stainless.yml` file for all SDK and docs configuration. This reduces cognitive overhead because you have one place to define targets, resources, and publishing settings. Fern splits configuration across multiple files, each scoped to a different concern. ### Validation Stainless shows [diagnostics](/docs/reference/diagnostics/index.md) in the Studio and CLI output if we detect problems with or potential improvements in your OpenAPI spec or Stainless config. An AI agent based workflow helps you fix diagnostics in your OpenAPI spec and config, and you can also use the [Stainless VS Code extension](/docs/guides/iterate-with-lsp/index.md) to get diagnostics feedback directly in your editor. \~193 diagnostics span across 23 categories, covering issues from OpenAPI schema problems to suboptimal SDK design patterns. Fern CLI has a `check` command that validates your project. Validation covers four areas: YAML/JSON syntax, OpenAPI schema correctness, reference integrity, and generator configuration. ### Custom code Stainless enables you to add code anywhere in your SDK and it will stay there. You commit custom code directly to your SDK repository, and Stainless preserves your changes across regeneration cycles. See [Add custom code](/docs/guides/add-custom-code/index.md) for details. Fern uses a `.fernignore` file to protect custom files from being overwritten. The trade-off: any file in `.fernignore` is frozen at the point you added it. If Fern improves retry logic or error handling in a protected file, that improvement never reaches your SDK. ### OpenAPI customization When your OpenAPI spec needs adjustments for SDK generation, Stainless provides [transforms](/docs/reference/transforms/index.md) that modify your spec during the generation pipeline without changing the source file. Stainless transforms provide feedback when they become unused, preventing configuration from silently drifting out of sync with your spec. Fern uses an `overrides.yml` file for the same purpose. For a deeper look at how transforms work, see [Stainless Transforms: The Fast Lane for Fixing Imperfect OpenAPI Specs](https://stainless.com/blog/stainless-transforms-the-fast-lane-for-fixing-imperfect-openapi-specs). ## Dependencies The number of runtime dependencies in your generated SDKs affects installation size, security surface area, and compatibility with different environments. Fewer dependencies mean a smaller attack surface, faster installs, and fewer version conflicts for your SDK consumers. | Language | Stainless | Fern | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | TypeScript | 0 | *No package.json is generated; dependency count may be incomplete.* | | Python | 6* `httpx` * `pydantic` * `typing-extensions` * `anyio` * `distro` * `sniffio` | 3* `httpx` * `pydantic` * `typing_extensions` | | Go | 2* `tidwall/gjson` * `tidwall/sjson` | 1* `google/uuid` | | Java | 11* `jackson-core` * `jackson-databind` * `jackson-annotations` * `jackson-datatype-jdk8` * `jackson-datatype-jsr310` * `jackson-module-kotlin` * `error_prone_annotations` * `httpcore5` * `httpclient5` * `okhttp` * `logging-interceptor` | 6* `okhttp` * `jackson-databind` * `jackson-core` * `jackson-annotations` * `jackson-datatype-jdk8` * `jackson-datatype-jsr310`*No build file (build.gradle.kts) is generated; dependencies inferred from imports.* | | Ruby | 2* `cgi` * `connection_pool` | 0 | | PHP | 5* `php-http/discovery` * `psr/http-client` * `psr/http-client-implementation` * `psr/http-factory-implementation` * `psr/http-message` | 2* `ext-json` * `guzzlehttp/guzzle` | | C# / .NET / Unity | 2–4* `System.Text.Json` * `System.Net.ServerSentEvents` * `Microsoft.SourceLink.GitHub` *(build-time only)* * `System.Collections.Immutable` *(netstandard2.0 only)* | 5–6* `OneOf` * `OneOf.Extended` * `System.Text.Json` * `System.Net.Http` * `System.Text.RegularExpressions` * `Portable.System.DateTimeOnly` *(net462/netstandard2.0 only)* | ## Size The table below shows the on-disk size of each installed SDK package. Stainless optimizes for size at the language level: TypeScript SDKs support opt-in tree shaking so bundlers can eliminate unused resources, and Python SDKs use lazy loading so imports are deferred until first use. | Language | Stainless | Fern | | ---------- | --------- | ---- | | TypeScript | 1.9M | 972K | | Python | 2.4M | 852K | | Go | 1.4M | 812K | | Java | 4.4M | 1.6M | | Ruby | 2.5M | 812K | | PHP | 1.6M | 696K | | C# / .NET | 2.3M | 1.3M | ## Documentation Stainless and Fern both generate API reference documentation from OpenAPI specifications, but they differ significantly in scope, customization, and hosting flexibility. Stainless provides a complete documentation platform with full code ownership, while Fern focuses on themed docs with configuration-based customization. | Feature | Stainless | Fern | | ------------------------------- | -------------------------------------- | --------------------------------- | | API reference | ✅ Auto-generated from OpenAPI | ✅ Auto-generated from OpenAPI | | SDK reference | ✅ Auto-generated per language | ❌ | | Prose documentation | ✅ Full Astro repo | ✅ Markdown with custom components | | Customization | ✅ Full code access (Astro) | ⚠️ Theme configuration | | Self-hosting | ✅ Any static host or Stainless hosting | ✅ | | Git-based workflow | ✅ | ✅ | | WYSIWYG editor | ❌ | ✅ | | Slack based AI technical writer | ❌ | ✅ | | Versioning | ✅ | ✅ | | Search | ✅ Built-in | ✅ Built-in | | AI chat | ✅ Built-in, customizeable | ✅ Built-in | ### Customization depth Stainless gives you a full Astro repository that you own and control. You can add React, Vue, Svelte, or other components, write custom CSS, install Astro plugins, and integrate any tool in the Astro ecosystem. Fern provides theming configuration for colors, logos, and layout, but you work within the boundaries of their configuration options rather than having direct access to the underlying code. ### Self-hosting You can deploy Stainless docs to any static hosting provider, including Vercel, Netlify, Cloudflare Pages, or your own infrastructure. Stainless also offers managed hosting if you prefer a hands-off approach. With Fern, self-hosting is available only on enterprise plans. ### Docs-as-code Both platforms support markdown-based documentation. Stainless fully embraces a git-based docs-as-code workflow. Your documentation lives in a repository you control, and you manage changes through pull requests, code review, and CI/CD, just like the rest of your codebase. Fern offers a WYSIWYG editor alongside optional git integration, which can be convenient for non-technical contributors. ### Which to choose Choose Stainless Docs Platform if - your team is comfortable with git - you need deep customization: custom components, design system integration - you want full control over build output - you want your developers to have an integrated SDK and API reference experience Choose Fern Docs if - your team includes non-technical writers who need to contribute without learning git - you prefer a WYSIWYG editor - you want a Slack-based writing assistant for content creation ## MCP servers Model Context Protocol (MCP) servers allow AI agents to interact with your API. Stainless generates fully functional MCP servers directly from your OpenAPI spec, while Fern does not currently offer MCP server generation. Stainless is the only platform in this comparison that generates MCP servers, giving your users a way to integrate your API with AI assistants out of the box. | Feature | Stainless | Fern | | --------------------- | ---------------------- | ---- | | MCP server generation | ✅ | ❌ | | Hosted deployment | ✅ (Cloudflare Workers) | ❌ | | Docker support | ✅ | ❌ | | Tool-level evals | ✅ | ❌ | Stainless MCP servers come with multiple deployment options. You can run them as hosted services on Cloudflare Workers for zero-infrastructure deployment, or package them as Docker containers for self-hosted environments. ## Next steps Both Fern and Stainless generate SDKs from OpenAPI specifications, but they differ in runtime features, design quality, developer workflow, and additional capabilities like documentation and MCP servers. The right choice depends on your priorities: whether you value zero-dependency output, hand-written SDK design patterns, or breadth of language support. - [Get started with Stainless](/docs/index.md) - Create your first SDK in minutes - [Migrate from Fern](/docs/resources/migrations/migrate-from-fern/index.md) - Step-by-step migration guide - [SDK configuration reference](/docs/reference/config/index.md) - Full configuration documentation