Skip to content Documentation index for agents (llms.txt)
Glotto Beta
Get started

Migrate from Fern

Fern was absorbed by Postman in January 2026. You keep the SDKs you already generated — but the pipeline that regenerated them on every spec change is no longer the product you bought. Glotto picks it up, and adds the part that is hard to rebuild in-house: every regeneration is verified, not just emitted — compile and contract statuses, per-target drift, and custom-code preservation, recorded in a verification report delivered to CI, the release PR, and the console.

The conversion is mechanical, complete, and reported line by line.

You don’t need anything from your old vendor. Fern is Apache-2.0, so fern/generators.yml, .fernignore and your OpenAPI document are already in your own git repo. That is the practical difference from migrating off Stainless, where the config has to be exported from a dashboard first.

What Glotto needs from you

Your fern/ directory — that is it. The OpenAPI document your api.specs[] block points at is read from there, resolved relative to generators.yml exactly as fern generate does.

How you get it

Unlike the Stainless conversion, this one does not run in the published CLI. It reads your API surface through the spec pipeline — the part of Glotto that stays on our infrastructure — and there is no self-serve route for it yet, so glotto migrate fern in the installed CLI exits 2 and says exactly that rather than half-running.

Email hello@glotto.dev with your fern/ directory and we run the conversion and hand back the glotto.yml and the report described below. Everything on this page is what that conversion does; nothing here is a preview of unbuilt work.

What you get back

A glotto.yml, plus a migration report saying what happened to every key.

Three choices are yours to make, and only one is ever forced:

  • Which generator group. Required when your project declares several and has no default-group — see Several generator groups below.
  • Which API document. By default the one your api.specs[] block names; supply another to override it.
  • Whether to pin member names to what your Fern generator produced, rather than take Glotto’s idiomatic casing. See Keep your SDK’s member names.

The conversion copies every recognized key, normalizes what differs, and is honest about the rest: nothing is silently invented or silently lost. Nothing already on disk is overwritten.

Several generator groups

A Fern project often has more than one group — say sdks and server. A default-group selects one and the migration just works. Without one, the migrator stops and names them rather than guessing:

groups error GLOTTO_MIGRATE_FERN_GROUP_AMBIGUOUS this project declares 2 generator
groups ("sdks", "server") and no `default-group`; name the group to migrate.

No file is written. Each group migrates into its own config instead — sdks into glotto.yml, server into server.glotto.yml.

Read the migration report

This is the part worth actually reading, and it is where Glotto’s Fern migration differs most from a hand-translation. Every key in your generators.yml is classified into exactly one of four verdicts — “no verdict” is not a state the report can express:

Verdict What it means
Mapped Translated into glotto.yml. The report names the key it became.
Already guaranteed You are not losing this. Glotto provides it unconditionally, so there is no config to carry across.
Not honored A Fern emission-shape knob Glotto answers differently on purpose. Reported with the reason.
Unmapped Genuinely no Glotto home. This is the list that needs your attention.

The already-guaranteed column is why the report has four verdicts rather than two. A two-way report would list these beside your genuine losses, telling you that you are giving up forward-compatible enums at the exact moment you are gaining a stronger, un-configurable version of them:

Your Fern setting Why you no longer need it
enable-forward-compatible-enums, respect-forward-compatible-enums, pydantic_config.enum_type: forward_compatible_python_enums Glotto emits open enums unconditionally — an unrecognized wire value round-trips instead of throwing. See Forward compatibility.
allowExtraFields, generate-unknown-as-json-node, pydantic_config.extra_fields: allow Glotto preserves unknown response fields unconditionally and round-trips them on re-send.
enable_wire_tests, enableWireTests, generate-mock-server-tests Glotto emits a test suite with every SDK, and its mock serves a spec-validated local server.

The not-honored column is the largest, and it is a deliberate non-goal rather than a gap to close later. noSerdeLayer, useBrandedStringAliases, neverThrowErrors, union: v0|v1, package-layout, the pydantic_config block — these configure Fern’s emitter. Glotto owns its own emission shape; honoring them would mean re-implementing Fern.

Timeouts are converted, not copied

Worth calling out on its own, because getting it wrong is invisible. Fern’s timeout key changes units per language:

Generator Key Unit
TypeScript defaultTimeout milliseconds
C# default-timeout-in-milliseconds milliseconds
Python timeout seconds

So a project declaring defaultTimeout: 30000 and timeout: 30 is declaring one timeout, and the migrator emits one: client_settings.default_timeout: 30s. Copying the numbers across instead would be a silent 1000× error in your production timeout — it validates clean, and only surfaces under load.

'infinity' (legal in TypeScript and Python) has no default_timeout spelling, so it is reported rather than coerced to some arbitrary large number you never chose.

Keep your SDK’s member names

A migration that renames your SDK’s public members breaks every consumer downstream. Two facts, and they are good news:

Your method names already survive. Glotto derives an emitted method name from the OpenAPI operationId and re-cases it per language — which is exactly Fern’s rule. listPets() stays listPets() in TypeScript and list_pets() in Python, with no configuration at all.

Your property names need a choice. Glotto keeps the wire spelling for an SDK member where Fern re-cases it — a wire property adoptedAt emits as adoptedAt in the Python SDK, where Fern emitted adopted_at. Ask for the Fern spelling to be pinned and the conversion emits the naming / parameter_naming entries that hold them there, and reports every name it pinned:

Pinned names: 3
  - property adopted_at → naming.Pet.adoptedAt.python
  - parameter starting_after → parameter_naming.listPets.startingAfter.python
  - parameter pet_id → parameter_naming.getPet.petId.python

It also refuses the pins that would break your build, and says so:

  • A property whose wire name is a reserved word in the target language. class is a Python keyword; Glotto escapes it to class_, and pinning the raw name back would emit a module that does not compile. The refusal is per language — Go has no class keyword, so it is pinned there.
  • A property containing an acronym, where casing conventions genuinely disagree (HTTPStatushttp_status or httpstatus?). Rather than guess and introduce the divergence it is supposed to remove, the migrator names both candidates and leaves the choice to you.

Anything Glotto cannot pin is reported naming both spellings — including the client class name, which has no glotto.yml key (every Glotto SDK exports Client). Nothing is left silently divergent.

.fernignore becomes something better

Fern’s .fernignore freezes a file: it stops being generated, and you stop receiving generator updates to it. That is a real cost — the file is yours now, including its bugs.

Glotto’s custom-code preservation three-way-merges instead, so you keep your edits and keep the updates. Because the two are semantically different, the migrator reports each .fernignore entry with the construct that replaces it rather than translating it, and writes no files:

  • Hand-written files belong in lib/, which Glotto writes if absent and never overwrites.
  • Generated files you have edited stay where they are — the checksum guard preserves your changes across regeneration.

Mapping reference

Fern Glotto Notes
api.specs[].openapi openapi.source Re-based onto the directory your glotto.yml is written to.
api.specs[].origin openapi.source Used when no local path is declared.
api.specs[].overlays transforms[].apply_overlay.source One apply_overlay entry per overlay document, in the order you listed them. Relative paths are re-based onto your glotto.yml’s directory.
api.specs[].overrides transforms[].merge_document.source One merge_document entry per overrides file, re-based onto your glotto.yml’s directory. This is deliberately not apply_overlay: an overrides file is an OpenAPI-shaped merge document, not an Overlay with actions[]. Your file carries across unchanged.
api.specs[].namespace Reported. Glotto takes one input source per glotto.yml.
api.specs[].git Reported. Glotto reads a path or URL; vendor the spec or point at its raw URL.
api.specs[].settings.filter.endpoints exclude Direct — Glotto’s exclude accepts the "POST /users" spelling.
api.specs[].settings.* Fern’s OpenAPI ingestion knobs; Glotto’s ingestion answers each with one behavior.
default-group / groups.<id> targets The selected group’s generators become your target map.
groups.<id>.generators[].name targets.<slug> By generator name. A language Glotto doesn’t generate is omitted and reported, never emitted as an invalid target.
generators[].output.package-name targets.<slug>.package_name The published registry id.
generators[].output.location Reported as the registry that target is published to.
generators[].github.repository targets.<slug>.repo (+ repo_provider) Feeds the multi-VCS release flow.
generators[].version Glotto versions the generator, not your config.
config.maxRetries / default_max_retries client_settings.retry.max_attempts See Retries & timeouts.
config.defaultTimeout / timeout / default-timeout-in-milliseconds client_settings.default_timeout Unit-converted — see above.
config.auto-generate-idempotency-key client_settings.idempotency See Idempotency.
config.namespace / namespaceExport / clientModuleName targets.<slug>.namespace The emitted code module — distinct from the registry id above.
config.client_class_name / clientName Reported. Every Glotto SDK exports Client; re-export it under your own name if consumers depend on it.
config.offsetSemantics Reported. Glotto models pagination per method, not per client.
.fernignore lib/ + checksum-guarded managed files Reported, not translated — see above.

Your docs migrate too

If your project has a fern/docs.yml, it migrates too — the branding into glotto.yml#/docs, the navigation tree into the generated site’s guides collection, and the page content into MDX the site can build. It is picked up automatically; the content tree lands in ./docs-site unless you say otherwise, and can be left out entirely if you want the SDK config alone.

The content tree obeys the same rule as the config: nothing already on disk is overwritten.

This is the part no other migration guide offers. Your docs have inbound links you do not control — StackOverflow answers, blog posts, bookmarks, other people’s READMEs — and moving a page breaks every one of them silently.

So the migration derives a redirect map. A Fern page’s URL comes from its navigation slug chain; a Glotto guide serves under /guides/…. Where those differ, you get a docs.redirects entry:

docs:
  redirects:
    - from: /introduction/overview
      to: /guides/introduction/overview

glotto generate turns that into two things, because one is not enough:

  1. public/_redirects — the rule file Cloudflare Workers Static Assets and Netlify read.
  2. A static redirect page at the old URL — a zero-second meta refresh with a canonical link at the target — so the promise holds on a host that reads no rule file at all.

The stubs carry no noindex, deliberately: a zero-second refresh is read as a permanent redirect and consolidates the old URL’s search ranking onto the new one, which is the whole point.

If you also set docs.deploy.target to vercel or netlify, you get a third: that host’s own redirect config (vercel.json or netlify.toml), so the status you declared is returned as a real HTTP status rather than an HTML refresh. It matters most on Vercel, which reads neither the rule file nor the stubs. A wildcard source is only translated into Vercel’s dialect when the translation is exact — a trailing * is, a * in the middle is not — and glotto generate tells you about the latter rather than rewriting it into a rule matching URLs you did not write.

Those two files are host config, not just redirect config, so regeneration leaves your own keys alone. If you add a build command, a headers block, or a framework override to vercel.json, only the redirects array is refreshed on the next glotto generate — every other key stays exactly as you wrote it, and the run tells you it kept them. netlify.toml is preserved the same way every other generated file you edit is: your version is kept and reported rather than overwritten, and glotto generate --merge merges the new redirects into it. In both cases --force gives you the generator’s file back.

A page whose URL doesn’t change produces no entry — a redirect from a path to itself is a no-op loop, and one per page would bury the moves that matter. You can add your own entries to docs.redirects at any time; glotto generate reports a source that is not site-absolute, duplicated, or pointing at itself, and a source that collides with a page the generator emits is refused so a redirect can never shadow real documentation.

Your components are translated, and nothing is dropped

Fern’s callouts land on the two components the generated site ships:

Fern Glotto
<Note> <Note>
<Info> / <Tip> / <Check> / <Success> <Note title="…">
<Warning> <Warning>
<Error> / <Danger> <Warning title="Error">
<Callout intent="…"> <Note> or <Warning>, by intent

The structural components have no Glotto counterpart, so each becomes the semantic HTML that carries the same meaning and renders with no component at all — <Accordion><details>/<summary>, <Steps> → an ordered list, <CardGroup> → a link list, <Frame><figure>/<figcaption>, <Tooltip><abbr>, <CodeBlocks> and <Tabs> → titled fences and headed sections. Every substitution is named in the report.

A component we do not recognize — including your own, from fern/components/ — is unwrapped, not deleted. MDX resolves component names when the site builds, so leaving the tag would fail the build; removing the content would lose your prose. You keep the prose, the report names the component, and you re-create it as an Astro or React component if you want the presentation back (the generated site is Astro + React). A self-closing one has no children to keep, so it becomes an MDX comment carrying its original source — visible in the file, inert on the page, never silently gone.

<EndpointRequestSnippet> and <EndpointResponseSnippet> are the one construct where “re-create it as a component” is the wrong answer: Glotto generates the page each one stood in for, with that operation’s request and response examples and an interactive Try-It panel already on it. So the migration names that page as the remedy, and can rewrite the embed into a link to it.

The route is not guessed. It is derived through the same pipeline that decides where a reference page goes, so a link and the page it points at cannot drift apart. An endpoint that names no operation in your OpenAPI document is not linked — it keeps the comment treatment and appears in the report with the endpoint named, because a migration that reports success while shipping dead links would be worse than one that says what it could not resolve. If your project declares versions:, the links carry the default version’s route prefix, matching where the versioned site actually serves those pages.

Fern’s playground switch needs no migration at all: the Try-It panel ships on every reference page Glotto generates, so it is reported as already guaranteed rather than as something you are losing.

What the docs migration reports rather than translates

docs.yml Glotto Note
instances[].custom-domain docs.deploy.custom_domain Direct.
instances[].url The *.docs.buildwithfern.com subdomain is Fern’s hosting — the thing you are leaving.
logo.light / .dark / .alt / .href docs.logo Direct, field for field.
colors.<token>.light docs.theme Becomes a --glotto-* custom property.
colors.<token>.dark docs.theme emits one :root block. Glotto ships its own dark palette; override it with a :root[data-theme="dark"] rule.
navigation section: / page: src/guides/** + order: Your sidebar order is reproduced.
navigation - api: Already guaranteed. Glotto generates the API reference from your spec on every run.
navigation - link: Reported. Use the header layout slot.
versions docs.versions Mapped, first marked default. Point each entry at its spec snapshot — Fern’s versions[].path names a docs config, not a spec, so a TODO placeholder is emitted with a warning.
tabs / products Reported. These are Fern chrome shapes, not a version axis; folding them into docs.versions would restructure your site.
redirects docs.redirects Carried across, alongside the derived entries.
favicon Reported. Drop your icon into the generated project’s public/ — files you add are never overwritten.
navbar-links / footer-links Reported. BaseLayout exposes named header and footer slots for exactly this.
layout / css / js / typography Reported. Fern’s shell chrome; Glotto emits its own layout and exposes theme tokens plus the layout slots.
title Reported. The site titles itself from your OpenAPI info.title and your organization.name.
Page title / description / subtitle / slug guide frontmatter description wins over subtitle (the guides collection has one prose field). A page with no title gets one synthesized, because a title-less guide fails the build.
Page keywords / image / hide-toc / layout Reported per page.

Next steps

The migrated config is validation-clean by construction, so the next step is the only one:

glotto generate     # your SDKs, docs site, and MCP server

The migrated glotto.yml fills any required field your Fern config cannot supply — your organization name and contact, and your production base URL, which Fern keeps in fern.config.json and its API definition rather than in generators.yml — with a clearly-marked TODO placeholder and a warning in the report. Replace those, then generate.