npm package: @flink-app/ts-source-to-json-schema
Description: A zero-dependency TypeScript-to-JSON Schema converter that uses its own mini-parser
Author: joel@frost.se
Latest version: 0.1.7

# ts-to-jsonschema

A zero-dependency TypeScript-to-JSON Schema converter that uses its own mini-parser instead of the TypeScript compiler.

It parses TypeScript type declarations (interfaces, type aliases, enums) directly from source strings and emits JSON Schema (2020-12 draft). No `ts.createProgram`, no `node_modules` bloat, no compiler startup cost.

## Why

Existing tools like `ts-json-schema-generator` and `typescript-json-schema` rely on the full TypeScript compiler API to resolve types. That works, but it's slow, heavy, and ties you to Node.js. Furthermore, these libraries might need significant rewrites when TypeScript 7 (tsgo) is released, making their long-term future uncertain.

This library takes a different approach: instead of running the type checker, it tokenizes and parses a **practical subset** of TypeScript's type syntax directly. The result is a tool that's fast enough to run in a hot path, portable enough to run anywhere (Cloudflare Workers, browser, CLI), and simple enough to extend.

The trade-off is explicit: it handles the type constructs you'd actually use in API contracts, tool schemas, and config definitions — not the full type-level programming language.

## What it handles

- **Declarations**: `interface`, `type`, `enum` (string + numeric), `export`
- **Primitives**: `string`, `number`, `boolean`, `null`, `undefined`, `any`, `unknown`, `never`, `void`, `bigint`
- **Built-in types**: `Date` → `{ type: "string", format: "date-time" }`
- **Literal types**: `"active"`, `42`, `true`
- **Unions**: `A | B | C` → `anyOf` (or `enum` when all members are literals)
- **Intersections**: `A & B` → `allOf`
- **Arrays**: `T[]`, `Array<T>`, nested `T[][]`
- **Tuples**: `[string, number]` → `prefixItems`
- **Nullable**: `string | null` → `type: ["string", "null"]`
- **Nested objects**: inline `{ foo: string }` and cross-references via `$ref`
- **Self-referential types**: `Task` containing `subtasks: Task[]`
- **Interface extends**: `interface Dog extends Animal` → `allOf`
- **Index signatures**: `[key: string]: T` → `additionalProperties`
- **Generic type instantiation**: `interface Foo extends Generic<TypeArg>` → inlines with type parameter substitution
  - Supports interfaces and type aliases with type arguments
  - Handles nested generics: `Box<Box<string>>`
  - Multiple type parameters: `Pair<T, U>`
  - Type parameters are substituted throughout properties, arrays, unions, etc.
  - Falls back to `$ref` when generic definition is not found
- **Utility types**: `Partial<T>`, `Required<T>`, `Pick<T, K>`, `Omit<T, K>`, `Record<K, V>`, `Readonly<T>`, `Set<T>`, `Map<K, V>`, `Promise<T>` (unwrapped)
- **Local imports**: Automatic resolution of relative imports (`./` and `../`) across files
- **JSDoc**: `/** description */` → `description`, plus tags: `@minimum`, `@maximum`, `@minLength`, `@maxLength`, `@pattern`, `@format`, `@default`, `@deprecated`, `@title`, `@example`, `@additionalProperties`
- **Readonly**: `readonly` → `readOnly` in schema

## Installation

```bash
npm install ts-source-to-json-schema
```

## CLI Usage

The package includes a command-line tool for converting TypeScript files to JSON Schema:

```bash
# Convert a TypeScript file
npx ts-source-to-json-schema src/types.ts

# Convert with automatic import resolution (default)
npx ts-source-to-json-schema src/api.ts

# Convert a specific type as the root schema
npx ts-source-to-json-schema src/api.ts --rootType ApiResponse

# Use strict mode (no additional properties allowed)
npx ts-source-to-json-schema src/config.ts --strictObjects

# Disable JSDoc processing
npx ts-source-to-json-schema src/types.ts --includeJSDoc false

# Single file mode, no imports
npx ts-source-to-json-schema src/types.ts --followImports none

# Combine multiple options
npx ts-source-to-json-schema src/user.ts -r User --strictObjects --followImports local
```

### CLI Options

```
-h, --help                     Show help message
-v, --version                  Show version number
    --doctor                   Output diagnostic information for debugging
    --batch                    Batch mode: generate schemas for all types across files

-r, --rootType <name>          Emit this type as root (others in $defs)
-s, --includeSchema <bool>     Include $schema property (default: true)
    --schemaVersion <url>      Custom $schema URL
    --strictObjects            Set additionalProperties: false globally
    --additionalProperties     Set additionalProperties default (true/false)
    --includeJSDoc <bool>      Include JSDoc comments (default: true)

--followImports <mode>         Follow imports: none, local, all (default: local)
--baseDir <path>               Base directory for resolving imports
```

The CLI reads TypeScript files and outputs JSON Schema to stdout, making it easy to pipe to files or other tools:

```bash
# Save to file
npx ts-source-to-json-schema src/types.ts > schema.json

# Pretty-print with jq
npx ts-source-to-json-schema src/types.ts | jq '.'

# Use in scripts
npx ts-source-to-json-schema src/api.ts --rootType Request > openapi/request-schema.json
```

### Multi-File Support (`--followImports`)

By default, the CLI automatically follows local relative imports (`./` and `../`) to resolve type definitions across multiple files:

```typescript
// pet.ts
export interface Pet {
  _id: string;
  name: string;
  species: string;
}

// api.ts
import { Pet } from './pet';
export interface PostPetReq extends Omit<Pet, "_id"> {}
```

```bash
# Follows imports and resolves Pet type (default behavior)
npx ts-source-to-json-schema api.ts --rootType PostPetReq

# Output includes both Pet (in $defs) and PostPetReq (as root)
```

**Import Resolution Modes:**
- `local` (default in CLI): Follows relative imports (`./` and `../`), skips `node_modules`
- `none`: Single-file mode, does not follow any imports
- `all`: Reserved for future `node_modules` support (currently behaves like `local`)

**Key Features:**
- Circular dependency detection (no infinite loops)
- Duplicate name detection (errors if same type name in multiple files)
- Automatic extension resolution (`.ts`, `.tsx`, `.d.ts`)
- Index file support (`./types` → `./types/index.ts`)

**Examples:**

```bash
# Multi-file project with local imports
npx ts-source-to-json-schema src/api.ts --followImports local

# Single file only, ignore imports
npx ts-source-to-json-schema src/standalone.ts --followImports none

# Custom base directory for import resolution
npx ts-source-to-json-schema src/api.ts --baseDir ./src
```

### Batch Mode (`--batch`)

Use `--batch` to generate schemas for all types across multiple files at once. Accepts glob patterns or multiple file paths:

```bash
# Batch: all schemas from matching files
npx ts-source-to-json-schema --batch 'src/schemas/**/*.ts' --followImports local

# Batch: specific files
npx ts-source-to-json-schema --batch src/PostReq.ts src/PostRes.ts --followImports local

# Batch with strict objects
npx ts-source-to-json-schema --batch 'src/types/*.ts' --strictObjects
```

Output is a JSON object where keys are type names and values are standalone schemas:

```json
{
  "PostReq": { "$schema": "...", "type": "object", "properties": { ... } },
  "PostRes": { "$schema": "...", "type": "object", "properties": { ... } }
}
```

Shared imports are resolved once and deduplicated across all entry files.

### Diagnostics Mode (`--doctor`)

When you encounter issues with schema conversion, use the `--doctor` flag to output comprehensive diagnostic information that can be shared with developers:

```bash
npx ts-source-to-json-schema src/problematic-types.ts --doctor
```

The doctor output includes:
- **Timestamp**: When the conversion was attempted
- **Environment**: Node.js version, platform, architecture, and current working directory
- **Input file details**: Path, existence, size, modification time, source length, and full source code
- **Options used**: All configuration options passed to the converter
- **Conversion result**: Either the successfully generated schema or detailed error information with stack traces

This makes it easy to:
1. Copy-paste the full diagnostic output when reporting issues
2. Debug why a particular TypeScript file isn't converting as expected
3. Share reproducible examples with maintainers

**Example output:**
```json
{
  "timestamp": "2026-02-25T10:50:55.680Z",
  "environment": {
    "nodeVersion": "v20.17.0",
    "platform": "darwin",
    "arch": "arm64",
    "cwd": "/path/to/project"
  },
  "input": {
    "filePath": "types.ts",
    "absolutePath": "/absolute/path/to/types.ts",
    "fileExists": true,
    "fileSize": 486,
    "sourceLength": 486,
    "source": "interface User { ... }"
  },
  "options": { "rootType": "User" },
  "conversionResult": {
    "success": true,
    "schema": { ... }
  }
}
```

## Programmatic Usage

### String-Based API

```typescript
import { toJsonSchema } from "ts-source-to-json-schema";

const schema = toJsonSchema(`
  /** Input for the ad analysis tool */
  interface AnalyzeAdInput {
    /** URL of the ad to analyze */
    url: string;
    /** Platform the ad is from */
    platform: "instagram" | "facebook" | "tiktok";
    /** Whether to extract color palette */
    extractColors?: boolean;
    /** Max elements to identify
     * @minimum 1
     * @maximum 50
     * @default 10
     */
    maxElements?: number;
    tags: string[];
  }
`, { rootType: "AnalyzeAdInput" });
```

### File-Based API with Import Resolution

```typescript
import { toJsonSchemaFromFile } from "ts-source-to-json-schema";

// Convert a TypeScript file with automatic import resolution
const schema = toJsonSchemaFromFile('./src/types/api.ts', {
  followImports: 'local',  // Follow relative imports
  rootType: 'ApiRequest',
  strictObjects: true
});

// Single-file mode (no import resolution)
const singleFileSchema = toJsonSchemaFromFile('./src/types.ts', {
  followImports: 'none'
});
```

### Batch Schema Generation

When you need schemas for multiple types from the same source file, use `toJsonSchemas()` (plural) for better performance. This is particularly useful for build tools, documentation generators, or API schema validators that process many types at once.

```typescript
import { toJsonSchemas } from "ts-source-to-json-schema";

const source = `
  export interface User { id: string; name: string; }
  export interface Post { id: string; title: string; author: User; }
  export interface Comment { id: string; text: string; }
`;

// Generate all schemas at once (~70% faster than calling toJsonSchema() for each type)
const schemas = toJsonSchemas(source);

console.log(schemas.User);    // Standalone User schema
console.log(schemas.Post);    // Post schema with User in definitions
console.log(schemas.Comment); // Standalone Comment schema
```

**Key benefits:**
- **Performance**: ~70% faster when generating 30+ schemas from the same source
- **Single parse**: Source is tokenized and parsed only once
- **Standalone schemas**: Each schema includes only the types it references in its `definitions` field
- **Self-contained**: Each schema includes `$schema` field by default for better IDE validation
- **Draft-07 compatible**: Uses `definitions` instead of `$defs` for broader compatibility

**How it works:**
- Parses the source once and emits all type declarations
- Each schema is standalone with minimal `definitions` (only transitively referenced types)
- Uses `#/definitions/TypeName` refs instead of `#/$defs/TypeName`
- Handles circular references, transitive dependencies, and utility types

**Use cases:**
- Framework integrations (like Flink) that need many schemas from one source
- Build tools generating schemas for entire API definition files
- Documentation generators processing large type files
- Schema validation libraries caching multiple schemas

**Example with real-world API:**
```typescript
const schemas = toJsonSchemas(`
  interface User { id: string; name: string; }
  interface CreateUserRequest { name: string; }
  interface CreateUserResponse { user: User; }
  interface GetUserRequest { id: string; }
  interface GetUserResponse { user: User; }
`, { includeJSDoc: true, strictObjects: true });

// Extract specific schemas as needed
const requestSchemas = {
  CreateUser: schemas.CreateUserRequest,
  GetUser: schemas.GetUserRequest
};
```

#### Batch Generation with Import Resolution

If your types are spread across multiple files with imports, use `toJsonSchemasFromFile()` (the file-based batch API):

```typescript
import { toJsonSchemasFromFile } from "ts-source-to-json-schema";

// Intermediate schema file (e.g., generated by a framework)
// schemas.ts:
//   import TreeNode from "../../src/schemas/TreeNode";
//   import Car from "../../src/schemas/Car";
//   export interface GetTree_12_ResSchema extends TreeNode {}
//   export interface GetCar_10_ResSchema extends Car {}

const schemas = toJsonSchemasFromFile('./schemas.ts', {
  followImports: 'local',  // Resolve relative imports
  strictObjects: true
});

console.log(schemas.GetTree_12_ResSchema);
// {
//   "$ref": "#/definitions/TreeNode",
//   "definitions": {
//     "TreeNode": { "type": "object", ... }  // ✅ Included from imported file!
//   }
// }
```

**Why use `toJsonSchemasFromFile()`?**
- Automatically resolves imports from local files
- Generates batch schemas for all types (like `toJsonSchemas()`)
- Handles circular dependencies across files
- Perfect for framework integrations (like Flink) with generated intermediate files

**Performance:** Same batch performance benefits as `toJsonSchemas()`, plus automatic import resolution.

#### Multi-Entry Batch Generation

When you need to generate schemas from multiple TypeScript files (not just one entry file), use `toJsonSchemasFromFiles()`. It accepts a glob pattern or an array of file paths:

```typescript
import { toJsonSchemasFromFiles } from "ts-source-to-json-schema";

// Glob pattern — matches all .ts files in src/schemas/
const schemas = toJsonSchemasFromFiles('src/schemas/**/*.ts', {
  followImports: 'local'
});

// Array of specific files
const schemas = toJsonSchemasFromFiles([
  'src/schemas/PostUserReq.ts',
  'src/schemas/PostUserRes.ts'
], { followImports: 'local' });

// With defineNameTransform for namespacing
const schemas = toJsonSchemasFromFiles('src/schemas/**/*.ts', {
  followImports: 'local',
  defineNameTransform: (name, decl, ctx) => {
    const file = path.basename(ctx?.relativePath ?? '', '.ts');
    return `${file}.${name}`;
  }
});
```

**Key benefits over `toJsonSchemasFromFile()`:**
- Accepts multiple entry files or a glob pattern (not just a single file)
- Shared imports are resolved once and deduplicated across all entry files
- Combined with `defineNameTransform`, enables stable type IDs across all schemas
- Returns `{}` when no files match (safe for empty globs)

**Glob support:**
- `*` matches any characters within a file/directory name
- `**` matches zero or more directory levels
- `?` matches a single character

Output:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "description": "Input for the ad analysis tool",
  "properties": {
    "url": { "type": "string", "description": "URL of the ad to analyze" },
    "platform": {
      "type": "string",
      "enum": ["instagram", "facebook", "tiktok"],
      "description": "Platform the ad is from"
    },
    "extractColors": { "type": "boolean", "description": "Whether to extract color palette" },
    "maxElements": {
      "type": "number",
      "description": "Max elements to identify",
      "minimum": 1,
      "maximum": 50,
      "default": 10
    },
    "tags": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["url", "platform", "tags"]
}
```

## Options

```typescript
toJsonSchema(source, {
  rootType: "MyType",            // Emit this type as the root schema (others go in $defs)
  includeSchema: true,           // Include $schema field (default: true)
  strictObjects: false,          // Set additionalProperties: false on all objects
  schemaVersion: "https://json-schema.org/draft/2020-12/schema",
  includeJSDoc: true,            // Include JSDoc descriptions and tags (default: true)
  additionalProperties: undefined, // Default value for additionalProperties (undefined, true, or false)
});

toJsonSchemaFromFile(filePath, {
  // All options from toJsonSchema, plus:
  followImports: 'local',        // Follow imports: 'none' (default for API), 'local' (default for CLI), 'all'
  baseDir: './src',              // Base directory for resolving imports (default: dirname(filePath))
  onDuplicateDeclarations: 'error', // Handle duplicate type names: 'error' (default), 'warn', 'silent'
});

toJsonSchemasFromFile(filePath, {
  // All options from toJsonSchemas, plus:
  followImports: 'local',        // Follow imports: 'none', 'local' (default), 'all'
  baseDir: './src',              // Base directory for resolving imports (default: dirname(filePath))
  onDuplicateDeclarations: 'error', // Handle duplicate type names: 'error' (default), 'warn', 'silent'
  // Note: rootType is not supported (generates schemas for all types)
});

toJsonSchemasFromFiles(entries, {
  // entries: glob pattern (string) or array of file paths
  // All options from toJsonSchemas, plus:
  followImports: 'local',        // Follow imports: 'none' (default), 'local', 'all'
  baseDir: './src',              // Base directory for resolving imports (default: cwd)
  onDuplicateDeclarations: 'error', // Handle duplicate type names: 'error' (default), 'warn', 'silent'
  // Note: rootType is not supported (generates schemas for all types)
});
```

### `includeJSDoc` (optional)
- **Type:** `boolean`
- **Default:** `true`
- **Description:** Controls whether JSDoc comments are processed and included in the schema

When `true` (default):
- Interface/type descriptions are extracted from JSDoc comments
- Property descriptions are included
- JSDoc tags like `@minimum`, `@maximum`, `@pattern` are applied as constraints

When `false`:
- All JSDoc comments are ignored
- Schema only contains structural information (types, properties, required fields)
- Useful for generating minimal schemas or when descriptions aren't needed

**Example:**
```typescript
const schema = toJsonSchema(`
  /** User profile */
  interface User {
    /** @minLength 1 */
    name: string;
  }
`, { rootType: 'User', includeJSDoc: false });

// Result: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }
// No description or minLength constraint
```

### `includeSchema` (optional)
- **Type:** `boolean`
- **Default:** `true`
- **Description:** Controls whether the `$schema` field is included in generated schemas

When `true` (default):
- Each generated schema includes a `$schema` field pointing to the JSON Schema specification
- Makes schemas self-contained and improves IDE validation support
- Uses the URL from `schemaVersion` option (default: `https://json-schema.org/draft/2020-12/schema`)

When `false`:
- The `$schema` field is omitted from all generated schemas
- Useful when embedding schemas in larger documents or when the schema version is managed elsewhere

**Example:**
```typescript
const schema = toJsonSchema(`
  interface User {
    name: string;
  }
`, { rootType: 'User', includeSchema: true });

// Result includes: { "$schema": "https://json-schema.org/draft/2020-12/schema", type: 'object', ... }
```

**Batch generation:** The `toJsonSchemas()` function also respects this option and includes `$schema` in each generated schema by default.

### `schemaVersion` (optional)
- **Type:** `string`
- **Default:** `"https://json-schema.org/draft/2020-12/schema"`
- **Description:** Specifies the JSON Schema draft version URL

Use this to generate schemas compatible with different JSON Schema drafts:

```typescript
// Generate draft-07 compatible schema
const schema = toJsonSchema(`interface User { name: string; }`, {
  rootType: 'User',
  schemaVersion: 'http://json-schema.org/draft-07/schema#'
});

// Result includes: { "$schema": "http://json-schema.org/draft-07/schema#", ... }
```

**Note:** This library always generates draft-2020-12 compatible schemas. The `schemaVersion` option only changes the `$schema` field value. For maximum compatibility with draft-07 validators, use `toJsonSchemas()` which uses `definitions` instead of `$defs`.

### `additionalProperties` (optional)
- **Type:** `boolean | undefined`
- **Default:** `undefined`
- **Description:** Sets the default value for `additionalProperties` on all object schemas

This option provides a global default for `additionalProperties` when not explicitly set via JSDoc or index signatures.

**Precedence (highest to lowest):**
1. Index signature: `[key: string]: T` → `additionalProperties: T`
2. JSDoc `@additionalProperties` tag
3. `strictObjects` option
4. `additionalProperties` option
5. Not set (JSON Schema default behavior)

**Example:**
```typescript
// Set all objects to disallow additional properties by default
const schema = toJsonSchema(`
  interface User {
    name: string;
  }
`, { rootType: 'User', additionalProperties: false });

// Result: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], additionalProperties: false }
```

### `@additionalProperties` JSDoc Tag

Control `additionalProperties` on specific types or properties using the `@additionalProperties` JSDoc tag.

**Supported values:** `true` | `false` (case-insensitive)

**Usage at interface/type level:**
```typescript
/**
 * Strict configuration object
 * @additionalProperties false
 */
interface Config {
  host: string;
  port: number;
}
// Result: { type: 'object', properties: {...}, additionalProperties: false }
```

**Usage at property level:**
```typescript
interface Settings {
  /**
   * Database configuration
   * @additionalProperties false
   */
  database: {
    host: string;
    port: number;
  };
}
// Result: database property has additionalProperties: false
```

**Interaction with other options:**
- When `includeJSDoc: false`, the tag is ignored
- The tag overrides the global `additionalProperties` and `strictObjects` options
- Index signatures take precedence over the tag

### `followImports` (optional)
- **Type:** `"none" | "local" | "all"`
- **Default:** `"none"` (programmatic API), `"local"` (CLI)
- **Description:** Controls import resolution across multiple TypeScript files

**Modes:**
- `none`: Single-file mode, imports are ignored
- `local`: Follows relative imports (`./` and `../`), skips `node_modules`
- `all`: Reserved for future `node_modules` support (currently behaves like `local`)

**Only available with file-based APIs** (`toJsonSchemaFromFile()`, `toJsonSchemasFromFile()`, and `toJsonSchemasFromFiles()`) — the string-based APIs (`toJsonSchema()` and `toJsonSchemas()`) do not support import resolution.

**Example:**
```typescript
// Given: pet.ts exports Pet interface
// Given: api.ts imports Pet and uses it in PostPetReq

const schema = toJsonSchemaFromFile('./api.ts', {
  followImports: 'local',
  rootType: 'PostPetReq'
});

// Result: Schema includes both Pet (in $defs) and PostPetReq
```

**Features:**
- Circular dependency detection (prevents infinite loops)
- Duplicate name handling (configurable with `onDuplicateDeclarations` option)
- Automatic extension resolution (`.ts`, `.tsx`, `.d.ts`)
- Index file resolution (`./types` → `./types/index.ts`)

### `baseDir` (optional)
- **Type:** `string`
- **Default:** `path.dirname(filePath)`
- **Description:** Base directory for resolving relative imports

Only relevant when `followImports` is not `"none"`.

### `onDuplicateDeclarations` (optional)
- **Type:** `"error" | "warn" | "silent"`
- **Default:** `"error"`
- **Description:** Controls how to handle duplicate type names across multiple files

**Only relevant when `followImports` is not `"none"`** (requires import resolution).

**Modes:**
- `error` (default): Throws an error when the same type name is found in multiple files
- `warn`: Uses the first declaration encountered and logs a warning to `console.warn`
- `silent`: Uses the first declaration encountered without any warning

**When to use:**
- `error`: Strict mode for catching unintentional duplicates (recommended for most cases)
- `warn`: Development mode where you want to be notified but not block execution
- `silent`: When you have intentional duplicates and want to use the first declaration

**Example:**
```typescript
// File 1: schemas/CampaignImage.ts
export interface ImageDimensions {
  width: number;
  height: number;
  format?: string;
}

// File 2: schemas/BrandAsset.ts
export interface ImageDimensions {
  width: number;
  height: number;
  format: string;  // Required (different from File 1)
}

// With error mode (default) - throws error
toJsonSchemaFromFile('entry.ts', {
  followImports: 'local'
});
// Error: Duplicate declaration "ImageDimensions" found in:
//   .../CampaignImage.ts
//   .../BrandAsset.ts

// With warn mode - uses first declaration
toJsonSchemaFromFile('entry.ts', {
  followImports: 'local',
  onDuplicateDeclarations: 'warn'
});
// Warning: Duplicate declaration "ImageDimensions" found...
// Uses ImageDimensions from CampaignImage.ts (format is optional)

// With silent mode - uses first declaration without warning
toJsonSchemaFromFile('entry.ts', {
  followImports: 'local',
  onDuplicateDeclarations: 'silent'
});
// No warning, uses ImageDimensions from CampaignImage.ts
```

**Best practice:** Keep the default `error` mode to catch duplicate names early. If you encounter duplicates:
1. Check if the types are structurally identical — if so, extract to a shared file
2. If they're different but have the same name, rename one of them for clarity
3. Use `warn` or `silent` mode only as a temporary workaround

### `defineId` (optional)
- **Type:** `(originalName: string, declaration: Declaration, context?: { absolutePath: string; relativePath: string }) => string`
- **Default:** `undefined`
- **Description:** Assigns a `$id` to each schema and uses external `$ref` instead of local `#/definitions/` refs

When set on batch APIs (`toJsonSchemas`, `toJsonSchemasFromFile`, `toJsonSchemasFromFiles`):
1. Each schema gets a `$id` field (the return value of the callback)
2. Internal `$ref` pointers become external refs pointing to the `$id` of the referenced type
3. The `definitions` block is omitted entirely
4. The `$id` is used as the key in the returned `Record<string, JSONSchema>`

This is designed for use with **AJV's global registry**, where schemas are registered by `$id` and cross-references are resolved at validation time.

**Example with AJV:**
```typescript
import { toJsonSchemas } from "ts-source-to-json-schema";
import Ajv from "ajv";

const schemas = toJsonSchemas(`
  interface User { id: string; name: string; }
  interface Post { title: string; author: User; }
`, {
  defineId: (name) => \`schemas.\${name}\`
});

// schemas = {
//   "schemas.User": {
//     "$id": "schemas.User",
//     "$schema": "...",
//     "type": "object",
//     "properties": { "id": { "type": "string" }, "name": { "type": "string" } }
//   },
//   "schemas.Post": {
//     "$id": "schemas.Post",
//     "$schema": "...",
//     "type": "object",
//     "properties": { "title": { "type": "string" }, "author": { "$ref": "schemas.User" } }
//   }
// }

// Register all schemas with AJV
const ajv = new Ajv();
for (const schema of Object.values(schemas)) {
  ajv.addSchema(schema);
}

// Validate — AJV resolves $ref: "schemas.User" from its global registry
const validate = ajv.getSchema("schemas.Post");
validate({ title: "Hello", author: { id: "1", name: "Joel" } }); // ✅
```

**With file context for namespacing:**
```typescript
const schemas = toJsonSchemasFromFiles('src/schemas/**/*.ts', {
  followImports: 'local',
  defineId: (name, decl, ctx) => {
    const dir = ctx?.relativePath.replace(/\.ts$/, '').replace(/\//g, '.') ?? '';
    return dir ? \`\${dir}.\${name}\` : name;
  }
});
```

**Can be combined with `defineNameTransform`** — both options are independent. `defineId` controls `$id`/external `$ref`, while `defineNameTransform` controls `$defs`/`#/definitions/` names.

**Throws** if the callback returns duplicate `$id` values for different types.

## What it doesn't handle

Anything that requires the type checker to evaluate:

- Conditional types (`T extends U ? X : Y`)
- Mapped types (`{ [K in keyof T]: ... }`)
- Template literal types (`` `${A}-${B}` ``)
- `typeof`, `keyof`, `infer`
- `node_modules` imports (planned for future)

**Generic type parameter limitations:**
- Currently assumes conventional type parameter names (`T`, `U`, `V`, `W`)
- Custom parameter names like `interface Box<TValue> { value: TValue }` won't work correctly (it would look for `T` instead of `TValue`)
- This works for 95%+ of real-world cases where generics use standard names
- Maximum 4 type parameters supported

If you need these, use `ts-json-schema-generator`. If your types look like API contracts and tool definitions, this is probably all you need.