Typra generates typed runtime contracts from TypeSpec. Use it when TypeSpec is the source of truth for shared model shapes and you need generated model code, serialization helpers, protocol interfaces, tests, documentation, and a JSON contract AST across multiple runtimes.
The first package in this repository is
@typra/emitter, a TypeSpec emitter and CLI extracted
from Prompty's generic emitter work. Typra is emitter/tooling infrastructure; it
does not own product-specific contracts or hand-authored runtime adapters. The
published package is
@typra/emitter on npm.
User-facing docs are available at https://typra.dev.
TypeSpec is good at declaring contracts. Typra turns those declarations into the runtime surfaces developers need to consume the contracts consistently:
- Generated types/classes/structs with JSON/YAML helpers where supported by the target.
- Polymorphic model loading from TypeSpec discriminators.
- Provider wire-name metadata with
@@knownAsand defaults with@@defaultFor. - Parse-only aliases for named string unions with
@parseAlias. - Generated protocol interfaces and optional compile-only test scaffolds.
- Deterministic generated metadata and verifier reports for CI review.
Product repositories should keep their domain TypeSpec files, runtime behavior, and adapters. Typra keeps the reusable emitter behavior.
npm install --save-dev @typra/emitter @typespec/compiler@1 @typespec/json-schema@1Typra supports @typespec/compiler and @typespec/json-schema across the
>=1.10.0 <2.0.0 range (validated in CI against 1.10.0 and 1.15.0). TypeSpec
versions outside that range produce a diagnostic during emit; use
allow-unsupported-typespec-version: true only when you intentionally accept
possible generated output churn.
Add the emitter to tspconfig.yaml:
emit:
- "@typra/emitter"
options:
"@typra/emitter":
emitter-output-dir: "{cwd}/generated"
root-object: "Todo.Contracts.TodoList"
root-namespace: "Todo.Contracts"
deterministic-output: true
emit-targets:
- type: TypeScript
output-dir: "generated/typescript"
test-dir: "generated/typescript/tests"
import-path: "../index"
- type: Python
output-dir: "generated/python"
test-dir: "generated/python/tests"
import-path: "todo_contracts"
- type: Java
output-dir: "generated/java"
test-dir: "generated/java/tests"
package-name: "todo.contracts"
- type: Markdown
output-dir: "generated/markdown"Create a TypeSpec entry point:
import "@typra/emitter";
namespace Todo.Contracts;
model TodoList {
name: string;
items: TodoItem[];
}
model TodoItem {
id: string;
title: string;
state: TodoState;
}
@parseAlias("done", #["complete", "completed"])
union TodoState {
open: "open";
done: "done";
archived: "archived";
}Compile with TypeSpec:
npx tsp compile ./path/to/main.tsp --config ./tspconfig.yamlTypra emits each requested language under its configured output-dir, writes
tests when test-dir is set, and always writes generated metadata under the
configured emitter output root.
Typra starts from root-object, resolves the reachable model graph, lowers it to
an internal contract AST, then emits the configured targets. The generated output
is intentionally split from hand-authored runtime code:
- Generated files include Typra ownership markers.
.typra-generated/manifest.jsonrecords generated files and metadata..typra-generated/report.jsonrecords emitted files, skipped empty outputs, hygiene policy, warnings, and stale generated-file cleanup decisions.json-ast/model.jsonrecords the lowered contract surface for schema evolution and verifier checks.
Set deterministic-output: true for committed generated output. That stabilizes
generated metadata, normalizes text artifacts to LF, trims trailing whitespace,
and keeps final newlines stable for CI diffs.
Generated targets are expected to follow one language-neutral runtime contract: required fields fail when missing, optional zero values stay distinct from absence, polymorphic dispatch behaves consistently, provider wire mappings never fall back to canonical names for unmapped providers, and keyed collections do not promise object-key order. See the docs page on runtime semantics for the full contract.
Typra adds decorators for runtime concerns that TypeSpec does not model by default:
@@knownAs(WireOptions.maxOutputTokens, "openai", "max_completion_tokens");
@@defaultFor(WireOptions.temperature, "openai", 0.2);
model WireOptions {
maxOutputTokens?: int32;
temperature?: float32;
}Common decorators:
| Decorator | Purpose |
|---|---|
@sample |
Supplies generated test/example data for a property. |
@abstract |
Marks a model as not directly instantiated. |
@@coerce |
Expands scalar input into an object during load. |
@@factory |
Generates factory constructors for a model. |
@@method |
Generates method signatures or protocol methods. |
@@knownAs |
Maps a property to provider-specific wire names. |
@@defaultFor |
Records provider-specific required defaults. |
@parseAlias |
Accepts alternate input strings for a canonical union value. |
@@protocol |
Marks a model as an emitted protocol/interface contract. |
@parseAlias is parse-only: loading accepts aliases, but saving emits the
canonical TypeSpec value.
The standard TypeSpec @encodedName("application/json", "wire_name") sets a
property's canonical dict/JSON/YAML key in every runtime's load and save paths;
generated member names stay idiomatic and derive from the property name. Other
mime types are ignored. @@knownAs maps that canonical key to a provider key, so
toWire/fromWire translate between the encoded name and the provider name.
The root emitter options are:
| Option | Purpose |
|---|---|
root-object |
Required fully qualified model to generate from. |
root-namespace |
Namespace used to resolve and emit the model graph. |
root-alias |
Alias for the generated root surface. |
additional-roots |
Extra fully qualified roots to generate. |
omit-models |
Model names to leave out of generation. |
schema-output-dir |
Reserved schema directory for future cleanup flows. |
deterministic-output |
Stable metadata and text hygiene for CI diffs. |
protected-paths |
Hand-authored paths recorded for verifier boundaries. |
hydration-zones |
Extension zones recorded for verifier checks. |
allow-unsupported-typespec-version |
Warn on unvalidated TypeSpec versions. |
emit-targets |
Language-specific output configuration. |
Each emit-targets entry has a required type and can set output-dir,
test-dir, format, import-path, package-name, namespace, alias,
enum-parsing, protocol-scaffolds, cancellation-token-path, and
native-serialization.
protocol-scaffolds: "compile-only" emits test-dir-only implementations that
prove generated @@protocol/@@method contracts compile. They intentionally
throw or reject when called and are not runtime fakes.
Rust targets can opt into case-insensitive enum/string-union parsing:
emit-targets:
- type: Rust
output-dir: "generated/rust"
enum-parsing: "case-insensitive"Preview — not production-ready. The
native-serializationframework variants (pydantic,jackson,serde,zod,codable) and the HTTP transport projections (fastapi,starlette,httpx,fetch) are experimental. Several are not yet idempotency-locked under the output hygiene guard, and their generated shape may still change. For production, rely on the core model surface —load/saveand the JSON/YAML helpers.
TypeScript targets can opt into native runtime validation artifacts with
native-serialization: "zod". The default is "none", so existing TypeScript
output does not import Zod. Zod mode emits class-attached schema,
wireSchema, and wireObjectSchemaWithoutName validators alongside the
generated classes. schema.parse(input) first canonicalizes through Typra's
authoritative load(input).save() path, then validates the saved wire shape, so
validation cannot silently drift from Typra load/save semantics. Consumers using
this opt-in mode must provide zod in the generated TypeScript project.
emit-targets:
- type: TypeScript
output-dir: "generated/typescript"
native-serialization: "zod"native-serialization: "standard-schema" is reserved for a future
Standard Schema-compatible TypeScript path and currently fails fast rather than
approximating behavior.
Other native interop modes are target-specific and also delegate to Typra's generated load/save contract instead of defining a second serializer:
| Target | native-serialization mode |
Contract |
|---|---|---|
| Python | "pydantic" |
Emits Pydantic v2 BaseModel types whose validation and dump entry points call Typra load, save, and to_json. |
| Java | "jackson" |
Emits Jackson annotations plus serializers/deserializers that call Typra save and load; consumers provide jackson-databind. |
| Rust | "serde" |
Emits cfg-gated Serialize/Deserialize impls that route through Typra to_value and load_from_value. |
| Swift | "codable" |
Emits explicit Codable conformance that bridges through Typra save and load. |
Methods can attach runtime effect metadata through the backward-compatible
eighth @method argument:
@@method(
EnginePermissionPort,
"authorize",
"Decision",
"Authorize one request",
#{ request: "Request" },
false,
false,
#{ runtimeCancellable: true, atomic: false, nonFatal: false }
);runtimeCancellable adds only a native runtime parameter (C#
CancellationToken, Go context.Context, TypeScript AbortSignal, Rust or
Python CancellationToken). It is not a TypeSpec model field or serialized
value. atomic and nonFatal are metadata/documentation flags only. Set
cancellation-token-path to the full runtime-native symbol path, for example
crate::engine::CancellationToken for Rust or
prompty.core.cancellation.CancellationToken for Python.
@typra/emitter currently emits:
TypeScript: runtime model surfaces, JSON/YAML helpers, generated tests, and Vitest-compatible protocol scaffold tests.Python: dataclass-style model surfaces, JSON/YAML helpers, import-pruned output, and generated tests.CSharp: C# model surfaces,System.Text.Jsonhelpers, and generated tests/scaffolds.Go: Go structs with JSON/YAML support, scalar shorthand helpers, propagated child-load errors, polymorphic dispatch, and generated tests.Java: Java model surfaces with JSON/YAML helpers and generated fixture tests.Rust: Rust model surfaces with optional case-insensitive enum parsing.Swift: SwiftPM package output with JSON/YAML helpers backed by Foundation and Yams, generated XCTest coverage, and protocol compile scaffolds.Markdown: reference documentation generated from the contract graph.- JSON AST:
json-ast/model.jsonemitted for every TypeSpec generation.
The fixture validation flow exercises TypeScript, Python, C#, Go, Java, Rust, Swift, Markdown, and JSON AST generation. Go fixture validation includes formatting, vet, tests, scalar coercion helper coverage, and executable conformance.
The package includes convenience binaries:
npx typra-generate --help
npx typra-generate --spec ./typespec/main.tsp --root-object Todo.Contracts.TodoList --output ./generated
npx typra-verify --baseline ./baseline --current ./generated
npx typra-consumer-smoke --config ./typra-smoke.jsontypra-generate wraps the TypeSpec emitter for quick local generation.
typra-verify compares generated metadata and reports breaking changes,
protected-path touches, stale output, schema evolution, and hydration seams. It
does not delete files.
The synthetic fixture is the best compact reference for supported shapes:
packages\typra-emitter\fixtures\tspconfig.yaml
packages\typra-emitter\fixtures\shapes\main.tsp
packages\typra-emitter\fixtures\shapes\model\events\session.tsp
packages\typra-emitter\fixtures\shapes\model\pipeline\harness.tsp
It covers scalars, arrays, records, nested models, discriminated polymorphism, closed and open string unions, parse aliases, scalar coercion, factories, methods, provider wire names, defaults, and protocols.
Run the same checks that CI runs:
npm ci
npm run build
npm test
npm run generate:fixtures
npm run check:toolchain
npm run validate:fixtures
npm run pack:dry-runCI also runs a TypeSpec compatibility matrix for each explicitly supported
@typespec/compiler, @typespec/http, and @typespec/json-schema version pair
(currently 1.10.0 and 1.15.0). Add a matrix row when validating a new TypeSpec
version, and keep the package peer range (>=1.10.0 <2.0.0) in sync.
npm run check:toolchain reports the local tools needed by
validate:fixtures: Node.js, npm, uv run --python 3.12 --with pydantic --with pytest --with PyYAML python, Go/gofmt, Java/javac, .NET, Swift, and
Cargo. It intentionally gates fixture validation, not the TypeScript build or
unit suite. Run it before interpreting fixture-validation failures so missing
or outdated local toolchains do not look like emitter regressions.
validate:fixtures also runs the save-side conformance oracle: each target
runtime loads the canonical fixture sample, saves it back to a normalized
payload, and compares that output across the seven target backends. The
conformance matrix in packages/typra-emitter/fixtures/conformance-matrix.json
must list every semantic rule against every backend as implemented or as an
issue-cited waiver, and generated-test failures flow through bidirectional
known-failure maps so stale waivers fail once fixed.
After building, check the local CLI with:
node packages\typra-emitter\dist\src\cli.js --help@typra/emitter is published from the GitHub Actions Publish workflow using
npm trusted publishing. Release checklist:
- Update
packages\typra-emitter\package.jsonto a version that has not been published. - Run
npm ci,npm run build,npm test,npm run generate:fixtures,npm run validate:fixtures, andnpm run pack:dry-run. - Confirm
npm pack --workspace @typra/emitter --dry-run --jsonincludes the package README, thetypra-generatebin, and only intended package files. - Commit, push, and merge the change to
main. - Run the
Publishworkflow in GitHub Actions.