This guide explains how to create custom plugins for Schemantic to extend its functionality.
Schemantic's plugin system allows you to:
- Transform OpenAPI schemas before processing
- Customize type generation logic
- Add additional generators (e.g., validation schemas, documentation)
- Modify generated code after creation
- Add new CLI commands and options
Every plugin must implement the SchemanticPlugin interface:
interface SchemanticPlugin {
name: string;
version: string;
description: string;
// Optional lifecycle hooks
beforeSchemaLoad?: (context: GenerationContext) => Promise<void>;
afterSchemaLoad?: (
schema: OpenAPIV3.Document,
context: GenerationContext
) => Promise<void>;
beforeTypeGeneration?: (
typeName: string,
schema: any,
context: GenerationContext
) => Promise<void>;
afterTypeGeneration?: (
typeName: string,
generatedType: GeneratedType,
context: GenerationContext
) => Promise<void>;
beforeClientGeneration?: (context: GenerationContext) => Promise<void>;
afterClientGeneration?: (
generatedClient: GeneratedClient,
context: GenerationContext
) => Promise<void>;
beforeOutput?: (
files: GeneratedFile[],
context: GenerationContext
) => Promise<void>;
afterOutput?: (
files: GeneratedFile[],
context: GenerationContext
) => Promise<void>;
// Optional transformers
transformSchema?: (
schema: OpenAPIV3.Document,
context: GenerationContext
) => OpenAPIV3.Document;
transformType?: (
generatedType: GeneratedType,
context: GenerationContext
) => GeneratedType;
transformClient?: (
generatedClient: GeneratedClient,
context: GenerationContext
) => GeneratedClient;
// Optional custom generators
customTypeGenerators?: { [key: string]: TypeGenerator };
customOutputGenerators?: OutputGenerator[];
// Optional CLI extensions
cliCommands?: CliCommand[];
cliOptions?: CliOption[];
}interface GenerationContext {
config: SchemanticConfig;
schema: OpenAPIV3.Document;
outputDir: string;
logger: Logger;
utilities: PluginUtilities;
}interface GeneratedType {
name: string;
content: string;
dependencies: string[];
exports: string[];
isInterface: boolean;
isEnum: boolean;
isUnion: boolean;
sourceSchema: any;
}interface GeneratedClient {
className: string;
content: string;
methods: GeneratedMethod[];
dependencies: string[];
exports: string[];
}interface GeneratedFile {
path: string;
content: string;
type: "types" | "client" | "hooks" | "other";
}import { SchemanticPlugin, GenerationContext, GeneratedType } from "schemantic";
const addCommentsPlugin: SchemanticPlugin = {
name: "add-comments",
version: "1.0.0",
description: "Adds helpful comments to generated types",
afterTypeGeneration: async (
typeName: string,
generatedType: GeneratedType,
context: GenerationContext
) => {
// Add a comment at the top of each generated type
const comment = `/**\n * Generated type: ${typeName}\n * Source: ${context.schema.info.title}\n */\n`;
generatedType.content = comment + generatedType.content;
},
};
export default addCommentsPlugin;import { SchemanticPlugin, GenerationContext } from "schemantic";
import { OpenAPIV3 } from "openapi-types";
const schemaTransformPlugin: SchemanticPlugin = {
name: "schema-transformer",
version: "1.0.0",
description: "Transforms schema before processing",
transformSchema: (
schema: OpenAPIV3.Document,
context: GenerationContext
): OpenAPIV3.Document => {
// Example: Add custom properties to all schemas
if (schema.components?.schemas) {
Object.values(schema.components.schemas).forEach((schemaObj: any) => {
if (schemaObj.type === "object" && schemaObj.properties) {
// Add a metadata field to all objects
schemaObj.properties._metadata = {
type: "object",
description: "Metadata added by plugin",
properties: {
generatedAt: { type: "string", format: "date-time" },
version: { type: "string" },
},
};
}
});
}
return schema;
},
};
export default schemaTransformPlugin;import { SchemanticPlugin, GenerationContext, TypeGenerator } from "schemantic";
const customGeneratorPlugin: SchemanticPlugin = {
name: "validation-generator",
version: "1.0.0",
description: "Generates validation schemas alongside types",
customTypeGenerators: {
"validation-schema": (schema: any, context: GenerationContext) => {
const typeName = schema.title || "UnknownType";
const validationName = `${typeName}ValidationSchema`;
// Generate Zod validation schema
let validationContent = `export const ${validationName} = z.object({\n`;
if (schema.properties) {
Object.entries(schema.properties).forEach(
([propName, propSchema]: [string, any]) => {
const zodType = mapOpenAPIToZod(propSchema);
const isRequired = schema.required?.includes(propName);
validationContent += ` ${propName}: ${zodType}${
isRequired ? "" : ".optional()"
},\n`;
}
);
}
validationContent += "});\n\n";
validationContent += `export type ${typeName}ValidationInput = z.infer<typeof ${validationName}>;\n`;
return {
name: validationName,
content: validationContent,
dependencies: ["zod"],
exports: [validationName, `${typeName}ValidationInput`],
isInterface: false,
isEnum: false,
isUnion: false,
sourceSchema: schema,
};
},
},
};
function mapOpenAPIToZod(schema: any): string {
switch (schema.type) {
case "string":
if (schema.format === "email") return "z.string().email()";
if (schema.format === "uuid") return "z.string().uuid()";
return "z.string()";
case "number":
case "integer":
return "z.number()";
case "boolean":
return "z.boolean()";
case "array":
return `z.array(${mapOpenAPIToZod(schema.items)})`;
default:
return "z.unknown()";
}
}
export default customGeneratorPlugin;import { SchemanticPlugin, CliCommand } from "schemantic";
const cliExtensionPlugin: SchemanticPlugin = {
name: "cli-extension",
version: "1.0.0",
description: "Adds custom CLI commands",
cliCommands: [
{
name: "analyze",
description: "Analyze OpenAPI schema complexity",
options: [
{ name: "url", alias: "u", description: "Schema URL", type: "string" },
{
name: "output",
alias: "o",
description: "Output file",
type: "string",
},
],
action: async (options: any) => {
console.log("Analyzing schema complexity...");
// Implementation here
},
},
],
};import {
SchemanticPlugin,
OutputGenerator,
GenerationContext,
} from "schemantic";
const docGeneratorPlugin: SchemanticPlugin = {
name: "doc-generator",
version: "1.0.0",
description: "Generates API documentation",
customOutputGenerators: [
{
name: "api-docs",
generate: async (context: GenerationContext) => {
const { schema, outputDir } = context;
let markdownContent = `# ${schema.info.title} API Documentation\n\n`;
markdownContent += `Version: ${schema.info.version}\n\n`;
markdownContent += `${schema.info.description || ""}\n\n`;
// Generate endpoint documentation
if (schema.paths) {
markdownContent += "## Endpoints\n\n";
Object.entries(schema.paths).forEach(
([path, pathItem]: [string, any]) => {
Object.entries(pathItem).forEach(
([method, operation]: [string, any]) => {
if (operation && typeof operation === "object") {
markdownContent += `### ${method.toUpperCase()} ${path}\n\n`;
markdownContent += `${
operation.summary || operation.description || ""
}\n\n`;
}
}
);
}
);
}
return [
{
path: `${outputDir}/API_DOCUMENTATION.md`,
content: markdownContent,
type: "other" as const,
},
];
},
},
],
};import { SchemanticPlugin, GenerationContext } from "schemantic";
interface PluginConfig {
addTimestamps: boolean;
customPrefix: string;
excludePatterns: string[];
}
class ConfigurablePlugin implements SchemanticPlugin {
name = "configurable-plugin";
version = "1.0.0";
description = "A configurable plugin example";
private config: PluginConfig;
constructor(config: Partial<PluginConfig> = {}) {
this.config = {
addTimestamps: true,
customPrefix: "",
excludePatterns: [],
...config,
};
}
afterTypeGeneration = async (
typeName: string,
generatedType: any,
context: GenerationContext
) => {
if (this.shouldExclude(typeName)) {
return;
}
if (this.config.addTimestamps) {
const timestamp = `// Generated on ${new Date().toISOString()}\n`;
generatedType.content = timestamp + generatedType.content;
}
if (this.config.customPrefix) {
generatedType.name = `${this.config.customPrefix}${generatedType.name}`;
}
};
private shouldExclude(typeName: string): boolean {
return this.config.excludePatterns.some((pattern) =>
new RegExp(pattern).test(typeName)
);
}
}
// Usage
const plugin = new ConfigurablePlugin({
addTimestamps: true,
customPrefix: "My",
excludePatterns: ["^Internal.*", ".*Error$"],
});
export default plugin;Schemantic provides utilities to help with plugin development:
interface PluginUtilities {
// String utilities
toCamelCase(str: string): string;
toPascalCase(str: string): string;
toSnakeCase(str: string): string;
// Type utilities
isOptionalProperty(property: any, required: string[]): boolean;
getTypeFromSchema(schema: any): string;
resolveReference(ref: string, document: OpenAPIV3.Document): any;
// File utilities
ensureDirectoryExists(path: string): Promise<void>;
writeFile(path: string, content: string): Promise<void>;
// Validation utilities
validateSchema(schema: any): { isValid: boolean; errors: string[] };
validateGeneratedType(type: GeneratedType): {
isValid: boolean;
errors: string[];
};
}// my-plugin.js
module.exports = {
name: "my-plugin",
version: "1.0.0",
description: "My custom plugin",
afterTypeGeneration: async (typeName, generatedType, context) => {
// Plugin logic here
},
};Load with CLI:
npx schemantic plugin load ./my-plugin.js
npx schemantic generate --plugins my-plugin// @my-org/Schemantic-plugin/index.js
const plugin = {
name: "npm-plugin",
version: "1.0.0",
description: "NPM distributed plugin",
transformSchema: (schema, context) => {
// Transform logic
return schema;
},
};
module.exports = plugin;Load with CLI:
npm install @my-org/Schemantic-plugin
npx schemantic plugin load @my-org/Schemantic-plugin
npx schemantic generate --plugins npm-pluginimport { Schemantic, SchemanticConfig } from "schemantic";
import myPlugin from "./my-plugin";
const config: SchemanticConfig = {
schemaUrl: "http://localhost:8000/openapi.json",
outputDir: "./generated",
plugins: [{ name: "my-plugin", enabled: true }],
};
const Schemantic = new Schemantic(config);
const pluginManager = Schemantic.getPluginManager();
// Register custom plugin
pluginManager.registerPlugin(myPlugin);
pluginManager.enablePlugin("my-plugin");
// Generate with plugin
const result = await Schemantic.generate();import { describe, it, expect } from "jest";
import myPlugin from "./my-plugin";
import { GenerationContext, GeneratedType } from "schemantic";
describe("MyPlugin", () => {
it("should add comments to generated types", async () => {
const mockContext: GenerationContext = {
config: {},
schema: { info: { title: "Test API" } },
outputDir: "./test",
logger: console,
utilities: {},
} as any;
const generatedType: GeneratedType = {
name: "TestType",
content: "export interface TestType { id: string; }",
dependencies: [],
exports: ["TestType"],
isInterface: true,
isEnum: false,
isUnion: false,
sourceSchema: {},
};
await myPlugin.afterTypeGeneration?.(
"TestType",
generatedType,
mockContext
);
expect(generatedType.content).toContain("/**");
expect(generatedType.content).toContain("Generated type: TestType");
});
});import { Schemantic } from "schemantic";
import myPlugin from "./my-plugin";
describe("Plugin Integration", () => {
it("should work with full generation process", async () => {
const Schemantic = new Schemantic({
schemaFile: "./test-schema.json",
outputDir: "./test-output",
});
const pluginManager = Schemantic.getPluginManager();
pluginManager.registerPlugin(myPlugin);
pluginManager.enablePlugin("my-plugin");
const result = await Schemantic.generate();
expect(result.success).toBe(true);
// Verify plugin effects in generated files
const typesContent = result.generatedFiles.find(
(f) => f.type === "types"
)?.content;
expect(typesContent).toContain("Generated type:");
});
});const robustPlugin: SchemanticPlugin = {
name: "robust-plugin",
version: "1.0.0",
description: "Example of robust error handling",
afterTypeGeneration: async (typeName, generatedType, context) => {
try {
// Plugin logic that might fail
generatedType.content = processContent(generatedType.content);
} catch (error) {
context.logger.warn(
`Plugin failed for type ${typeName}: ${error.message}`
);
// Don't throw - allow generation to continue
}
},
};const performantPlugin: SchemanticPlugin = {
name: 'performant-plugin',
version: '1.0.0',
description: 'Example of performance-conscious plugin',
// Cache expensive computations
private cache = new Map<string, any>();
transformSchema: (schema, context) => {
const cacheKey = JSON.stringify(schema);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const transformed = expensiveTransform(schema);
this.cache.set(cacheKey, transformed);
return transformed;
}
};import * as Joi from "joi";
const configSchema = Joi.object({
enabled: Joi.boolean().default(true),
options: Joi.object({
prefix: Joi.string().default(""),
includeComments: Joi.boolean().default(true),
}),
});
const validatedPlugin: SchemanticPlugin = {
name: "validated-plugin",
version: "1.0.0",
description: "Plugin with configuration validation",
beforeSchemaLoad: async (context) => {
const pluginConfig = context.config.plugins?.find(
(p) => p.name === "validated-plugin"
);
if (pluginConfig) {
const { error } = configSchema.validate(pluginConfig);
if (error) {
throw new Error(`Invalid plugin configuration: ${error.message}`);
}
}
},
};my-Schemantic-plugin/
├── package.json
├── index.js
├── README.md
├── LICENSE
└── test/
└── plugin.test.js
{
"name": "@my-org/Schemantic-plugin-name",
"version": "1.0.0",
"description": "Description of your plugin",
"main": "index.js",
"keywords": ["Schemantic", "plugin", "openapi", "typescript"],
"peerDependencies": {
"schemantic": "^1.0.0"
},
"files": ["index.js", "README.md", "LICENSE"]
}Consider adding your plugin to the community registry by submitting a PR to the Schemantic plugins repository.
Popular community plugins include:
@Schemantic/plugin-zod- Generates Zod validation schemas@Schemantic/plugin-react-query- Generates React Query hooks@Schemantic/plugin-swagger-ui- Generates Swagger UI documentation@Schemantic/plugin-mock-data- Generates mock data factories