TypeGen — TypeScript & OpenAPI from C#
Roslyn source generator that turns C# DTOs into TypeScript interfaces and an
OpenAPI 3.0 schema document at compile time. One attribute on a class,
dotnet build, and the files land in your configured output directory.
Why not NSwag / Reinforced.Typings? Both rely on reflection over the built assembly — they need a compiled DLL loaded at post-build time, and (for NSwag) a running ASP.NET host to emit OpenAPI. TypeGen runs inside the C# compiler via an incremental source generator: no reflection, no running app,
dotnet buildproduces the artifacts directly. Cheap in CI/CD, Hot-Reload-friendly in IDE, statically verifiable in analyzers.
See the working sample: SampleApi on GitHub — ASP.NET Core app exercising all three endpoint-discovery paths side by side (
[CrudApi]synthesis + hand-written Minimal API withMapGroup+ hand-written[ApiController]).dotnet buildemitsgenerated/openapi.yamlshowing how all three sources contribute to one unifiedpaths:block.
Topics
Section titled “Topics”- Type mapping — default C# → TS / OpenAPI / Python / Zod,
[TsType]with imports,[UseType<T>]cross-target override - Configuration & output —
ITypeGenConfiguratorfluent DSL, open-generic targeting, companion-DTO synthesis, precedence, on-disk write pipeline - Diagnostic reference — every
TG00xxID - Validation → OpenAPI — DataAnnotations /
[Z…]→ schema constraints - Endpoint discovery — Minimal API scan, native controllers,
[CrudApi]synthesis - Polymorphism & interfaces —
[JsonPolymorphic]discriminated unions, opt-inEmitInterfaces - Advanced type features —
[JsonExtensionData], computed/immutable props, string-enum converters, transitive nested-type discovery, inheritance rules - Python emitter — Pydantic v2 / dataclasses
- Zod emitter — runtime validation schemas
Install
Section titled “Install”dotnet add package ZibStack.NET.TypeGenQuick start
Section titled “Quick start”using ZibStack.NET.TypeGen;
[GenerateTypes(Targets = TypeTarget.TypeScript | TypeTarget.OpenApi, OutputDir = "../client/src/api")]public class Order{ public int Id { get; set; } public string Customer { get; set; } = ""; public decimal Total { get; set; } public OrderStatus Status { get; set; } public List<OrderItem> Items { get; set; } = new();}
[GenerateTypes(Targets = TypeTarget.TypeScript | TypeTarget.OpenApi, OutputDir = "../client/src/api")]public class OrderItem{ public string Sku { get; set; } = ""; public int Quantity { get; set; } public decimal UnitPrice { get; set; }}
[GenerateTypes(Targets = TypeTarget.TypeScript | TypeTarget.OpenApi, OutputDir = "../client/src/api")]public enum OrderStatus { Pending, Shipped, Delivered, Cancelled }dotnet build produces:
// @generated by ZibStack.NET.TypeGen — do not edit
import { OrderItem } from './OrderItem';import { OrderStatus } from './OrderStatus';
export interface Order { id: number; customer: string; total: string; // decimal → string (preserves precision) status: OrderStatus; items: OrderItem[];}openapi: 3.0.3info: title: API version: 1.0.0paths: {}components: schemas: Order: type: object required: [Id, Customer, Total, Status, Items] properties: Id: { type: integer, format: int32 } Customer: { type: string } Total: { type: number, format: double } Status: { $ref: '#/components/schemas/OrderStatus' } Items: type: array items: { $ref: '#/components/schemas/OrderItem' } # OrderItem, OrderStatus elided for brevityPer-target overrides
Section titled “Per-target overrides”All overrides are plain attributes — no configuration file needed for the common cases.
TypeScript
Section titled “TypeScript”[TsName("OrderModel")] // rename the emitted interfacepublic class Order{ [TsName("orderId")] // force camelCase override [TsType("string")] // force a specific TS type expression public int Id { get; set; }
[TsIgnore] // skip this property in TS only public string InternalAuditId { get; set; } = "";}OpenAPI
Section titled “OpenAPI”[OpenApiSchemaName("OrderV1")] // rename in components/schemaspublic class Order{ [OpenApiProperty(Format = "uri", Example = "https://example.com/callback", Description = "Webhook URL delivered after dispatch.")] public string Callback { get; set; } = "";
[OpenApiIgnore] // skip in OpenAPI only public string InternalDebugField { get; set; } = "";}[TsIgnore] and [OpenApiIgnore] are independent — a property can appear in one
target and be hidden in the other.
For the full type-expression override story (including [TsType] with imports
and the cross-target [UseType<T>] attribute), see
Type mapping.
File layout & cross-file imports
Section titled “File layout & cross-file imports”Default TypeScript layout is one file per class/enum. Cross-type references emit
real import { X } from './X'; statements at the top of each file, so the output
compiles directly with tsc and the tooling in the consumer (ts-node, Vite,
webpack) resolves everything without a barrel file. Switch to a single bundled
file via TypeScriptSettings.FileLayout = TypeScriptFileLayout.SingleFile.
Why OpenAPI 3.0 (not 3.1)
Section titled “Why OpenAPI 3.0 (not 3.1)”The default emitted openapi: version is 3.0.3. Most tooling (Swashbuckle,
NSwag codegen, Redoc, Stoplight, the Microsoft.OpenApi.Readers 1.6.x lib used
by many .NET projects) still targets 3.0 — emitting 3.1 would quietly break
those consumers. Nullable types use the 3.0 nullable: true form; $ref
siblings are wrapped in allOf when nullable (the standard 3.0 workaround).
You can override via OpenApiSettings.OpenApiVersion.
Limitations (MVP)
Section titled “Limitations (MVP)”- Generic types are out of scope (diagnostic
TG0003). Apply[GenerateTypes]to closed types only. - Authorization on emitted paths isn’t yet mapped from
[CrudApi]policies to OpenAPIsecurity/securitySchemes.
See project_typegen_backlog on GitHub
for the full roadmap (Kotlin, Swift, Go, Dart, JSON Schema, authorization →
OpenAPI security).