Skip to content

TypeGen — TypeScript & OpenAPI from C#

NuGet Source

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 build produces 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 with MapGroup + hand-written [ApiController]). dotnet build emits generated/openapi.yaml showing how all three sources contribute to one unified paths: block.

dotnet add package ZibStack.NET.TypeGen
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:

Order.ts
// @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.yaml
openapi: 3.0.3
info:
title: API
version: 1.0.0
paths: {}
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 brevity

All overrides are plain attributes — no configuration file needed for the common cases.

[TsName("OrderModel")] // rename the emitted interface
public 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; } = "";
}
[OpenApiSchemaName("OrderV1")] // rename in components/schemas
public 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.

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.

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.

  • 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 OpenAPI security / securitySchemes.

See project_typegen_backlog on GitHub for the full roadmap (Kotlin, Swift, Go, Dart, JSON Schema, authorization → OpenAPI security).