This guide explains how to create custom plugins for Schemantic to extend its functionality.
Plugins in Schemantic allow you to:
- Transform schemas before type generation
- Modify generated types and API clients
- Add custom type generators
- Add custom client generators
- Execute code at various stages of the generation process
All plugins must implement the SchemanticPlugin interface:
interface SchemanticPlugin {
name: string;
version: string;
description: string;
// Lifecycle hooks
beforeGeneration?(context: GenerationContext): Promise<void> | void;
afterGeneration?(
context: GenerationContext,
result: GenerationResult
): Promise<void> | void;
// Type generation hooks
beforeTypeGeneration?(
typeName: string,
schema: ResolvedSchema,
context: GenerationContext
): Promise<void> | void;
afterTypeGeneration?(
typeName: string,
generatedType: GeneratedType,
context: GenerationContext
): Promise<void> | void;
// Client generation hooks
beforeClientGeneration?(context: GenerationContext): Promise<void> | void;
afterClientGeneration?(
generatedClient: GeneratedApiClient,
context: GenerationContext
): Promise<void> | void;
// Schema transformation hooks
transformSchema?(
schema: ResolvedSchema,
context: GenerationContext
): ResolvedSchema;
// Custom generators
customTypeGenerators?: Record<
string,
(schema: ResolvedSchema, context: GenerationContext) => GeneratedType
>;
customClientGenerators?: Record<
string,
(context: GenerationContext) => GeneratedApiClient
>;
}Here's a minimal plugin:
import { SchemanticPlugin, GenerationContext, GeneratedType } from "schemantic";
const myPlugin: SchemanticPlugin = {
name: "my-plugin",
version: "1.0.0",
description: "My custom plugin",
beforeGeneration: async (context: GenerationContext) => {
console.log("Starting generation...");
},
afterGeneration: async (context: GenerationContext, result: any) => {
console.log("Generation completed!");
},
};Called before the generation process starts. Use this to:
- Validate configuration
- Set up plugin-specific state
- Modify the generation context
beforeGeneration: async (context: GenerationContext) => {
// Validate configuration
if (!context.config.customOption) {
throw new Error('Custom option is required');
}
// Set up plugin state
context.pluginState = { myData: 'value' };
},Called after the generation process completes. Use this to:
- Clean up resources
- Generate additional files
- Post-process results
afterGeneration: async (context: GenerationContext, result: any) => {
// Generate additional files
if (result.success) {
await generateAdditionalFiles(context);
}
// Clean up
delete context.pluginState;
},Note: The react-hooks built-in plugin is temporarily disabled in the default plugin lists due to known generation issues that can corrupt output for some schemas. To re-enable it for a specific project, add { name: 'react-hooks', enabled: true } to your plugins array in schemantic.config.*, and run the generator on a small sample schema first to validate output.
Called before generating a specific type. Use this to:
- Transform the schema
- Add metadata
- Skip generation for certain types
beforeTypeGeneration: async (typeName: string, schema: ResolvedSchema, context: GenerationContext) => {
// Add custom metadata
schema._customMetadata = { processed: true };
// Skip certain types
if (typeName.includes('Internal')) {
return;
}
},Called after generating a specific type. Use this to:
- Modify the generated type content
- Add imports
- Post-process the type
afterTypeGeneration: async (typeName: string, generatedType: GeneratedType, context: GenerationContext) => {
// Add custom imports
generatedType.content = `import { CustomType } from './custom';\n\n${generatedType.content}`;
// Add custom decorators
if (generatedType.isInterface) {
generatedType.content = generatedType.content.replace(
/export interface (\w+)/g,
'@CustomDecorator\nexport interface $1'
);
}
},Called before generating API clients. Use this to:
- Set up client-specific configuration
- Modify the generation context
beforeClientGeneration: async (context: GenerationContext) => {
// Add custom configuration
context.clientConfig = {
baseUrl: 'https://api.example.com',
timeout: 30000,
};
},Called after generating API clients. Use this to:
- Modify the generated client
- Add custom methods
- Post-process the client
afterClientGeneration: async (generatedClient: GeneratedApiClient, context: GenerationContext) => {
// Add custom methods
const customMethods = `
async customMethod(): Promise<string> {
return 'custom result';
}
`;
generatedClient.content = generatedClient.content.replace(
/}\s*$/,
`${customMethods}\n}`
);
},Transform schemas before type generation. Use this to:
- Add custom properties
- Modify type information
- Standardize schema format
transformSchema: (schema: ResolvedSchema, context: GenerationContext): ResolvedSchema => {
// Add custom properties
if (schema.properties) {
for (const [propName, propSchema] of Object.entries(schema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) {
// Add custom metadata
propSchema._customProperty = true;
// Transform specific types
if (propSchema.format === 'uuid') {
propSchema._customType = 'CustomUuid';
}
}
}
}
return schema;
},Add custom type generators for specific schema types:
customTypeGenerators: {
'custom-type': (schema: ResolvedSchema, context: GenerationContext): GeneratedType => {
return {
name: 'CustomType',
content: `export type CustomType = string;`,
dependencies: [],
exports: ['CustomType'],
isInterface: false,
isEnum: false,
isUnion: false,
sourceSchema: schema,
};
},
'date-time': (schema: ResolvedSchema, context: GenerationContext): GeneratedType => {
return {
name: 'DateTime',
content: `export type DateTime = string; // ISO 8601 format`,
dependencies: [],
exports: ['DateTime'],
isInterface: false,
isEnum: false,
isUnion: false,
sourceSchema: schema,
};
},
},Add custom client generators:
customClientGenerators: {
'react-query': (context: GenerationContext): GeneratedApiClient => {
const hooks = generateReactQueryHooks(context);
return {
name: 'ReactQueryHooks',
content: hooks,
dependencies: ['react-query'],
exports: ['useApiHooks'],
endpoints: [],
};
},
'axios': (context: GenerationContext): GeneratedApiClient => {
const client = generateAxiosClient(context);
return {
name: 'AxiosClient',
content: client,
dependencies: ['axios'],
exports: ['AxiosApiClient'],
endpoints: [],
};
},
},Adds JSDoc comments to generated types:
const jsdocPlugin: SchemanticPlugin = {
name: "jsdoc",
version: "1.0.0",
description: "Adds JSDoc comments to generated types",
afterTypeGeneration: async (
typeName: string,
generatedType: GeneratedType,
context: GenerationContext
) => {
if (!generatedType.content.includes("/**")) {
const comment = `/**\n * ${
generatedType.sourceSchema.description || `Generated type: ${typeName}`
}\n */\n`;
generatedType.content = comment + generatedType.content;
}
},
};Adds validation decorators for class-validator:
const validationPlugin: SchemanticPlugin = {
name: "validation",
version: "1.0.0",
description: "Adds validation decorators to generated types",
afterTypeGeneration: async (
typeName: string,
generatedType: GeneratedType,
context: GenerationContext
) => {
if (generatedType.isInterface) {
// Add validation imports
const imports = `import { IsString, IsNumber, IsOptional, IsNotEmpty } from 'class-validator';\n\n`;
// Add validation decorators to properties
let content = generatedType.content;
content = content.replace(
/(\w+)(\?)?: (\w+);/g,
(match, propName, optional, type) => {
const decorators = [];
if (type === "string") decorators.push("@IsString()");
if (type === "number") decorators.push("@IsNumber()");
if (optional) decorators.push("@IsOptional()");
else decorators.push("@IsNotEmpty()");
return `${decorators.join("\n ")}\n ${match}`;
}
);
generatedType.content = imports + content;
}
},
};Generates React hooks for API endpoints:
const reactHooksPlugin: SchemanticPlugin = {
name: "react-hooks",
version: "1.0.0",
description: "Generates React hooks for API endpoints",
customClientGenerators: {
"react-hooks": (context: GenerationContext): GeneratedApiClient => {
const hooks = generateReactHooks(context);
return {
name: "ApiHooks",
content: hooks,
dependencies: ["react-query"],
exports: ["useApiHooks"],
endpoints: [],
};
},
},
};
function generateReactHooks(context: GenerationContext): string {
let hooks = `import { useQuery, useMutation, useQueryClient } from 'react-query';\n`;
hooks += `import { ApiClient } from './api-client';\n\n`;
hooks += `export const useApiHooks = (client: ApiClient) => {\n`;
hooks += ` const queryClient = useQueryClient();\n\n`;
// Generate hooks for each endpoint
for (const [path, pathItem] of Object.entries(context.schema.paths)) {
if (typeof pathItem === "object" && pathItem !== null) {
const operations = ["get", "post", "put", "delete", "patch"] as const;
for (const method of operations) {
const operation = pathItem[method];
if (operation) {
const hookName = generateHookName(
operation.operationId || `${method}${path}`
);
const hook = generateHook(operation, method, path);
hooks += ` ${hookName} = ${hook};\n`;
}
}
}
}
hooks += `};\n`;
return hooks;
}import { PluginLoader } from "schemantic";
const loader = new PluginLoader();
const plugin = await loader.loadPluginFromFile("./my-plugin.js");const plugin = await loader.loadPluginFromPackage("my-Schemantic-plugin");const plugins = await loader.loadPluginsFromDirectory("./plugins");import { Schemantic } from "schemantic";
const Schemantic = new Schemantic(config);
const pluginManager = Schemantic.getPluginManager();
pluginManager.registerPlugin(myPlugin);
pluginManager.enablePlugin("my-plugin");const config: SchemanticConfig = {
// ... other config
plugins: [
{ name: "my-plugin", enabled: true },
{ name: "jsdoc", enabled: true },
],
};npx schemantic generate --plugins my-plugin,jsdocAlways handle errors gracefully:
beforeTypeGeneration: async (typeName: string, schema: ResolvedSchema, context: GenerationContext) => {
try {
// Plugin logic
} catch (error) {
console.warn(`Plugin ${this.name} failed for type ${typeName}:`, error);
// Don't throw - let generation continue
}
},Consider performance implications:
// Cache expensive operations
const cache = new Map();
transformSchema: (schema: ResolvedSchema, context: GenerationContext): ResolvedSchema => {
const cacheKey = JSON.stringify(schema);
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = expensiveTransformation(schema);
cache.set(cacheKey, result);
return result;
},Document your plugin thoroughly:
const myPlugin: SchemanticPlugin = {
name: "my-plugin",
version: "1.0.0",
description: `
My custom plugin that adds special functionality.
Features:
- Adds custom decorators
- Transforms specific types
- Generates additional files
Usage:
Enable via configuration or CLI: --plugins my-plugin
`,
// ... implementation
};Test your plugins:
import { Schemantic, SchemanticConfig } from "schemantic";
describe("My Plugin", () => {
it("should transform schemas correctly", async () => {
const config: SchemanticConfig = {
schemaData: testSchema,
outputDir: "./test-output",
plugins: [{ name: "my-plugin", enabled: true }],
};
const Schemantic = new Schemantic(config);
const result = await Schemantic.generate();
expect(result.success).toBe(true);
expect(result.generatedFiles).toHaveLength(1);
});
});Use semantic versioning for your plugins:
const myPlugin: SchemanticPlugin = {
name: "my-plugin",
version: "1.2.3", // Major.Minor.Patch
description: "My plugin description",
// ... implementation
};- Create a package.json:
{
"name": "Schemantic-my-plugin",
"version": "1.0.0",
"description": "My custom Schemantic plugin",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": ["Schemantic", "plugin", "typescript", "openapi"],
"peerDependencies": {
"schemantic": "^1.0.0"
}
}- Export your plugin:
// src/index.ts
export { myPlugin } from "./plugin";
export * from "./types";- Build and publish:
npm run build
npm publishUsers can install and use your plugin:
npm install Schemantic-my-pluginimport { myPlugin } from "Schemantic-my-plugin";
import { Schemantic } from "schemantic";
const Schemantic = new Schemantic(config);
Schemantic.getPluginManager().registerPlugin(myPlugin);- Plugin not loading: Check that the plugin exports the correct interface
- Type errors: Ensure your plugin implements all required interface properties
- Performance issues: Use caching and avoid expensive operations in hooks
- Conflicts: Check for naming conflicts with other plugins
Enable debug mode to see plugin execution:
const config: SchemanticConfig = {
// ... other config
plugins: [{ name: "my-plugin", enabled: true, options: { debug: true } }],
};Add logging to your plugin:
beforeTypeGeneration: async (typeName: string, schema: ResolvedSchema, context: GenerationContext) => {
console.log(`[My Plugin] Processing type: ${typeName}`);
// Plugin logic
console.log(`[My Plugin] Completed processing: ${typeName}`);
},