A smithy-build plugin that
turns a Smithy model into a single self-contained TypeScript file: zod
schemas + types for every reachable shape, typed HTTP clients for your
alloy#simpleRestJson services, and typed mock
stubs for driving those services in Storybook.
From one model, into a single generated.ts:
- Data types — a
XxxSchema(zod) +type Xxx = z.infer<typeof XxxSchema>for every reachable structure, union, enum, list, map, and simple-type alias (aliases are.brand<'Xxx'>()-ed for nominal typing). - Error classes — one
XxxError extends Errorper@errorshape, carrying the declared HTTP status and error-type name. - Transport — a small
Transportinterface plus request/response types you implement once (overfetch, axios, …). The generated clients are transport-agnostic. - Service clients — one
XxxClientclass per service, oneasyncmethod per operation. Each method walks the operation's@httptrait to build the request (URI labels, query, headers, body) and parses the response with the generated output schema, dispatching declared errors by status (andX-Error-Typewhen several errors share a status). - Storybook mock stubs — a typed
XxxHandlersinterface +XxxMockdescriptor per service, plus a sharedmockServiceruntime, so a story can implement just the operations it exercises.
Add the artifact to your smithy-build classpath and reference the plugin by name (ts-codegen):
{
"version": "1.0",
"plugins": {
"ts-codegen": {
"outFile": "generated.ts",
"excludeServices": ["myorg.auth#AuthService"]
}
}
}Settings:
outFile— destination filename within the plugin's output dir (defaultgenerated.ts).excludeServices— fully-qualified service shape ids (namespace#Name) to skip. Their referenced data shapes are still emitted; only the operations and the client class are dropped. Use this for services you hand-roll (streaming, custom routing, …).
Add the sbt plugin (project/plugins.sbt):
addSbtPlugin("org.polyvariant" % "sbt-smithy-ts-codegen" % "<version>")Enable it on a project and point it at your smithy sources:
enablePlugins(SmithyTsCodegenPlugin)
tsCodegenSmithyDirs := Seq(baseDirectory.value / "src" / "main" / "smithy")
tsCodegenOutputFile := baseDirectory.value / "src" / "generated.ts"
tsCodegenExcludeServices := Seq("myorg.auth#AuthService")then run tsCodegen. The plugin resolves the smithy-ts-codegen-cli artifact (at the plugin's
own version) with coursier and runs it in a forked JVM, so nothing about your project's Scala
version affects the codegen. Settings:
tsCodegenSmithyDirs— dirs scanned for*.smithy/*.json(defaultsrc/main/smithy).tsCodegenOutputFile— where to write the TypeScript (defaulttarget/generated.ts).tsCodegenExcludeServices— service shape ids to skip (see above).tsCodegenVersion— override the codegen version to resolve (defaults to the plugin's own).
The core is a pure function from a loaded software.amazon.smithy.model.Model to a String:
import org.polyvariant.smithy.ts.TsCodegenPlugin
import software.amazon.smithy.model.Model
val model: Model = ???
val ts: String = TsCodegenPlugin.generate(model, excludeServices = Set.empty)The smithy-ts-codegen-cli artifact's org.polyvariant.smithy.ts.cli.Main assembles a model
from .smithy/.json sources and writes the output file. Run it with the CLI (and its deps) on
the classpath:
Main <smithyDirs (path-separator-joined)> <outFile> [<excludeServices (comma-joined)>]
This is what the sbt plugin forks; the smithy-build plugin is discovered via the SPI.
- Only
alloy#simpleRestJsonservices get clients; every operation needs an@httptrait. - Shapes in
smithy.api,smithy4s.*, andalloy.*namespaces, mixins, and trait definitions are not emitted as data types. - Recursive shapes are not supported — the generator topologically sorts shapes into one file and fails on cycles.
- Timestamps become
z.coerce.date(); blobs becomez.string().
smithy-build, smithy-codegen-core, smithy-model, alloy-core, and smithy4s-protocol.
Apache 2.0.