Schemas & Types
The generator converts TypeScript types into JSON Schema components for the OpenAPI spec. Understanding the resolution pipeline helps you get the most out of automatic schema generation.
Resolution pipeline
When the generator encounters a type (from @SwaggerResponse, @Body(), @SwaggerRequestBody, etc. — the accepted union is exported as TSwaggerConfigType), it resolves it through the following pipeline:
| Input | Result |
|---|---|
Class with static toJsonSchema() | Named component in #/components/schemas/ |
Instance with toJsonSchema() | Named component |
Plain object ({ type: 'string', ... }) | Inline JSON Schema |
String, Number, Boolean | { type: 'string' }, { type: 'number' }, { type: 'boolean' } |
Date | { type: 'string', format: 'date-time' } |
Array | { type: 'array' } |
Object | { type: 'object' } |
[SomeType] (array literal) | { type: 'array', items: <resolved SomeType> } |
| Literal string/number/boolean | { type, const: value } |
| Zero-arg function | Invoked; the return value is resolved recursively |
Type-name strings are a shorthand
The string literals 'string', 'number', 'boolean', 'integer', 'object', and 'array' resolve to { type: <value> } (a type shorthand) — not to a const literal. Any other string becomes { const: value, type: 'string' }.
Atscript types
The recommended approach is to use Atscript .as files. The TypeScript plugin generates classes with static toJsonSchema(), so the swagger generator picks them up automatically:
// types/User.as
@label "User"
export interface UserDto {
@label "User ID"
id: string
@label "Display name"
name: string
@label "Email address"
@expect.pattern "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", "u", "Invalid email"
email: string
@label "Roles"
roles: string[]
}This produces a UserDto class that the generator registers as #/components/schemas/UserDto. Everywhere you use UserDto — in responses, request bodies, parameters — it appears as { $ref: '#/components/schemas/UserDto' }.
Schema deduplication
The generator uses a WeakMap to track type references. The same class used in multiple places always produces a single component with multiple $ref pointers:
@SwaggerResponse(UserDto) // → $ref: '#/components/schemas/UserDto'
@Get(':id')
find() { /* ... */ }
@SwaggerResponse([UserDto]) // → { type: 'array', items: { $ref: '#/components/schemas/UserDto' } }
@Get()
list() { /* ... */ }
@SwaggerResponse(201, UserDto) // → same $ref, no duplicate
@Post()
create(@Body() dto: CreateUserDto) { /* ... */ }When two different classes share the same name (e.g., User from different modules), the generator appends a numeric suffix: User, User_1, User_2.
$defs hoisting
When a schema's toJsonSchema() output includes $defs (named sub-schemas referenced via $ref: '#/$defs/Name'), the generator automatically hoists them into #/components/schemas/ and rewrites all $ref paths.
This commonly occurs with:
- Discriminated unions — Atscript unions with a discriminator property
- Nested types — Types that reference other named types internally
- Recursive types — Self-referencing schemas
Example: an Atscript discriminated union:
export interface Dog {
petType: "dog"
name: string
breed: string
}
export interface Cat {
petType: "cat"
name: string
indoor: boolean
}
export type Pet = Dog | CatPet.toJsonSchema() produces:
{
"oneOf": [
{ "$ref": "#/$defs/Dog" },
{ "$ref": "#/$defs/Cat" }
],
"discriminator": {
"propertyName": "petType",
"mapping": { "dog": "#/$defs/Dog", "cat": "#/$defs/Cat" }
},
"$defs": {
"Dog": { "type": "object", "properties": { ... } },
"Cat": { "type": "object", "properties": { ... } }
}
}The generator transforms this into:
#/components/schemas/Pet—oneOfwith rewritten$refs#/components/schemas/Dog— hoisted from$defs#/components/schemas/Cat— hoisted from$defs
Refs are rewritten at any nesting depth within the root schema tree — array items, object properties, oneOf/allOf/anyOf branches, discriminator.mapping — so Pet[] correctly references the same component schemas.
Sibling refs inside hoisted defs
Refs are not rewritten inside a hoisted definition: if Dog itself contained $ref: '#/$defs/Cat', that ref would remain as a dangling #/$defs/Cat in the hoisted Dog component. Keep cross-references in the root schema tree, or pre-resolve them in your toJsonSchema() output.
Auto-generated examples
toExampleData()
If a type exposes a static toExampleData() method, the generator calls it to populate the example field in the component schema:
class UserDto {
static toJsonSchema() {
return {
type: 'object',
properties: {
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
}
}
static toExampleData() {
return { name: 'Alice', email: 'alice@example.com' }
}
}Atscript @meta.example
Atscript types can generate toExampleData() automatically from @meta.example annotations. Enable it in your Atscript config:
// atscript.config.ts
import { defineConfig } from '@atscript/core'
import tsPlugin from '@atscript/typescript'
export default defineConfig({
entries: ['src/**/*.as'],
plugins: [
tsPlugin({
jsonSchema: 'bundle',
exampleData: true,
}),
],
})With this enabled, every .as type with @meta.example annotations exposes toExampleData(), and the generator picks it up automatically. See the Atscript docs for details.
Example priority
When multiple sources provide an example, this priority order applies:
examplefield already present intoJsonSchema()output (highest)@SwaggerExampledecorator on the DTO classtoExampleData()method (lowest — auto-generation fallback)
Metadata on schemas
Moost core decorators on DTOs are reflected in the generated component schemas:
| Decorator | Schema field |
|---|---|
@Label('...') | title |
@Description('...') | description |
@Id('...') | title (fallback) |
@Label('User')
@Description('Represents a registered user in the system')
class UserDto {
static toJsonSchema() { /* ... */ }
}Generated component:
{
"UserDto": {
"title": "User",
"description": "Represents a registered user in the system",
"type": "object",
"properties": { ... }
}
}Array shorthand
Wrap a type in an array literal to create an array schema:
@SwaggerResponse([UserDto])
@Get()
list() {
// Schema: { type: 'array', items: { $ref: '#/components/schemas/UserDto' } }
}This works anywhere a type is accepted — responses, request bodies, parameters.
Lazy resolution
Pass a zero-argument function to defer schema resolution:
@SwaggerResponse(() => UserDto)
@Get(':id')
find() { /* ... */ }This is useful for avoiding circular import issues — the function is invoked at mapping time, after all modules are loaded.