Converts Prisma/MySQL/PostgreSQL/SQLite schemas to Zod schemas, TypeScript interfaces, or Kysely type definitions
- Generates Zod schemas, Typescript interfaces or Kysely type definitions for MySQL, PostgreSQL, SQLite, and Prisma schemas
- Supports camelCase conversion
- Handles nullable, default, auto-increment and enum fields
- Supports custom type overrides via configuration or database comments
- Intelligently handles field nullability based on operation type (table, insertable, updateable, selectable)
- All fields in updateable schemas are automatically made optional
Install mutano
with npm
npm install mutano
Create user table:
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '@zod(z.string().min(10).max(255))', -- this will override the Zod type
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`profile_picture` varchar(255) DEFAULT NULL,
`metadata` json NOT NULL COMMENT '@ts(Record<string, unknown>)', -- this will override the TypeScript type
`role` enum('admin','user') NOT NULL,
PRIMARY KEY (`id`)
);
Use the mutano API:
import { generate } from 'mutano'
await generate({
origin: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'secret',
database: 'myapp',
overrideTypes: {
json: 'z.record(z.string())'
}
},
destinations: [{
type: 'zod',
useDateType: true,
useTrim: false,
nullish: false, // When true, nullable fields use nullish() instead of nullable()
folder: './generated',
suffix: 'schema'
}]
})
import { generate } from 'mutano'
await generate({
origin: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'secret',
database: 'myapp',
overrideTypes: {
json: 'z.record(z.string())'
}
},
destinations: [{
type: 'ts',
modelType: 'type', // Generate TypeScript type aliases instead of interfaces
folder: './types',
suffix: 'types'
}]
})
import { generate } from 'mutano'
await generate({
origin: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'secret',
database: 'myapp',
overrideTypes: {
json: 'z.record(z.string())'
}
},
destinations: [{
type: 'ts',
header: "import type { CustomType } from './types';\nimport type { BaseModel } from './models';"
}]
})
import { generate } from 'mutano'
await generate({
origin: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'secret',
database: 'myapp',
overrideTypes: {
json: 'z.record(z.string())'
}
},
destinations: [{
type: 'zod',
header: "import { z } from 'zod';\nimport { CustomValidator } from './validators';"
}]
})
import { generate } from 'mutano'
await generate({
origin: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'secret',
database: 'myapp',
overrideTypes: {
json: 'z.record(z.string())'
}
},
destinations: [{
type: 'kysely',
schemaName: 'Database', // Default is 'DB'
header: "import { Generated, ColumnType } from 'kysely';\nimport { CustomTypes } from './types';",
folder: './db/types',
suffix: 'db'
}]
})
import { generate } from 'mutano'
// Generate without writing to disk
const output = await generate({
origin: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'secret',
database: 'myapp'
},
destinations: [{
type: 'zod'
}],
dryRun: true // Return content instead of writing to files
})
// Output is an object where keys are filenames and values are file content
console.log(Object.keys(output)) // ['user.ts', 'product.ts', ...]
// You can access the content for a specific file
console.log(output['user.ts'])
import { generate } from 'mutano'
await generate({
origin: {
type: 'postgres',
host: '127.0.0.1',
port: 5432,
user: 'postgres',
password: 'secret',
database: 'myapp',
schema: 'public', // optional, defaults to 'public'
overrideTypes: {
jsonb: 'z.record(z.string())'
}
},
destinations: [{
type: 'zod',
useDateType: true
}]
})
import { generate } from 'mutano'
await generate({
origin: {
type: 'sqlite',
path: './myapp.db',
overrideTypes: {
json: 'z.record(z.string())'
}
},
destinations: [{
type: 'ts'
}]
})
import { generate } from 'mutano'
await generate({
origin: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
user: 'root',
password: 'secret',
database: 'myapp',
overrideTypes: {
json: 'z.record(z.string())'
}
},
destinations: [
{
type: 'zod',
useDateType: true,
folder: './generated/zod',
suffix: 'schema'
},
{
type: 'ts',
folder: './generated/types',
suffix: 'type'
},
{
type: 'kysely',
folder: './generated/kysely',
suffix: 'db'
}
]
})
This will generate all three types of output files for each table in your database, placing them in separate folders with appropriate suffixes.
The generator will create user.type.ts
, user.schema.ts
, and user.db.ts
files with the following contents:
import { z } from 'zod';
import { CustomValidator } from './validators';
export const user = z.object({
id: z.number().nonnegative(),
name: z.string().min(10).max(255),
username: z.string(),
password: z.string(),
profile_picture: z.string().nullable(),
role: z.enum(['admin', 'user']),
})
export const insertable_user = z.object({
name: z.string().min(10).max(255),
username: z.string(),
password: z.string(),
profile_picture: z.string().nullable(),
role: z.enum(['admin', 'user']),
})
export const updateable_user = z.object({
name: z.string().min(10).max(255).optional(),
username: z.string().optional(),
password: z.string().optional(),
profile_picture: z.string().nullable().optional(),
role: z.enum(['admin', 'user']).optional(),
})
export const selectable_user = z.object({
id: z.number().nonnegative(),
name: z.string(),
username: z.string(),
password: z.string(),
profile_picture: z.string().nullable(),
role: z.enum(['admin', 'user']),
})
export type userType = z.infer<typeof user>
export type InsertableUserType = z.infer<typeof insertable_user>
export type UpdateableUserType = z.infer<typeof updateable_user>
export type SelectableUserType = z.infer<typeof selectable_user>
import { CustomType } from './types';
import { BaseModel } from './models';
// TypeScript interfaces for user
export interface User {
id: number;
name: string;
username: string;
password: string;
profile_picture: string | null;
metadata: Record<string, unknown>; // Custom type from @ts comment
role: 'admin' | 'user';
}
export interface InsertableUser {
name: string | null; // Optional because it has a default value
username: string;
password: string;
profile_picture: string | null;
metadata: Record<string, unknown>; // Custom type from @ts comment
role: 'admin' | 'user';
}
export interface UpdateableUser {
name: string | null; // Optional for updates
username: string | null; // Optional for updates
password: string | null; // Optional for updates
profile_picture: string | null;
metadata: Record<string, unknown> | null; // Custom type from @ts comment, optional for updates
role: 'admin' | 'user' | null; // Optional for updates
}
export interface SelectableUser {
id: number;
name: string;
username: string;
password: string;
profile_picture: string | null;
metadata: Record<string, unknown>; // Custom type from @ts comment
role: 'admin' | 'user';
}
import { Generated, ColumnType, Selectable, Insertable, Updateable } from 'kysely';
// JSON type definitions
export type Json = ColumnType<JsonValue, string, string>;
export type JsonArray = JsonValue[];
export type JsonObject = {
[x: string]: JsonValue | undefined;
};
export type JsonPrimitive = boolean | number | string | null;
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
// Kysely type definitions for user
// This interface defines the structure of the 'user' table
export interface UserTable {
id: Generated<number>;
name: string;
username: string;
password: string;
profile_picture: string | null;
metadata: Json;
role: 'admin' | 'user';
}
// Define the database interface
export interface DB {
user: UserTable;
}
// Use these types for inserting, selecting and updating the table
export type User = Selectable<UserTable>;
export type NewUser = Insertable<UserTable>;
export type UserUpdate = Updateable<UserTable>;
{
"origin": {
"type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "secret",
"database": "myapp",
"overrideTypes": {
"json": "z.record(z.string())"
},
"ssl": {
"ca": "path/to/ca.pem",
"cert": "path/to/cert.pem",
"key": "path/to/key.pem"
},
} | {
"type": "postgres",
"host": "127.0.0.1",
"port": 5432,
"user": "postgres",
"password": "secret",
"database": "myapp",
"schema": "public",
"overrideTypes": {
"jsonb": "z.record(z.string())"
},
"ssl": {
"ca": "path/to/ca.pem",
"cert": "path/to/cert.pem",
"key": "path/to/key.pem"
},
} | {
"type": "sqlite",
"path": "path/to/database.db",
"overrideTypes": {
"json": "z.record(z.string())"
}
} | {
"type": "prisma",
"path": "path/to/schema.prisma",
"overrideTypes": {
"Json": "z.record(z.string())"
}
},
"destinations": [
{
"type": "zod",
"useDateType": true,
"useTrim": false,
"nullish": false, // When true, nullable fields use nullish() instead of nullable()
"requiredString": false, // When true, adds min(1) validation to non-nullable string fields
"header": "import { z } from 'zod';\nimport { CustomValidator } from './validators';",
"folder": "@zod",
"suffix": "table"
},
{
"type": "ts",
"enumType": "union",
"modelType": "interface",
"header": "import { CustomType } from './types';\nimport { BaseModel } from './models';",
"folder": "types",
"suffix": "type"
},
{
"type": "kysely",
"schemaName": "Database",
"header": "import { Generated, ColumnType } from 'kysely';\nimport { CustomTypes } from './types';",
"folder": "kysely",
"suffix": "db"
}
],
"tables": ["user", "log"],
"ignore": ["log", "/^temp/"],
"camelCase": false,
"silent": false,
"dryRun": false,
"magicComments": true
}
Option | Description |
---|---|
destinations | An array of destination configurations to generate multiple output formats from a single origin |
destinations[].type | The type of output to generate: "zod", "ts", or "kysely" |
destinations[].useDateType | (Zod only) Use a specialized Zod type for date-like fields instead of string |
destinations[].useTrim | (Zod only) Use z.string().trim() instead of z.string() |
destinations[].nullish | (Zod only) Use nullish() instead of nullable() for nullable fields. In updateable schemas, fields that were already nullable will become nullish |
destinations[].requiredString | (Zod only) Add min(1) for non-nullable string fields |
destinations[].enumType | (TypeScript only) How to represent enum types: "union" (default) or "enum" |
destinations[].modelType | (TypeScript only) How to represent models: "interface" (default) or "type" |
destinations[].schemaName | (Kysely only) Name of the database interface (default: "DB") |
destinations[].header | Custom header to include at the beginning of generated files (e.g., custom imports) |
destinations[].folder | Specify the output directory for the generated files |
destinations[].suffix | Suffix to the name of a generated file (eg: user.table.ts ) |
tables | Filter the tables to include only those specified |
ignore | Filter the tables to exclude those specified. If a table name begins and ends with "/", it will be processed as a regular expression |
camelCase | Convert all table names and their properties to camelcase. (eg: profile_picture becomes profilePicture ) |
silent | Don't log anything to the console |
dryRun | When true, doesn't write files to disk but returns an object with filenames as keys and generated content as values |
magicComments | Use @zod and @ts comments to override types (unsupported by SQLite) |
You can override the default type for a specific column type. This is specific to each database type and is placed inside the origin object. Each database type has its own set of valid types that can be overridden:
{
"origin": {
"type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "secret",
"database": "myapp",
"overrideTypes": {
"json": "z.record(z.string())",
"text": "z.string().max(1000)"
}
}
}
{
"origin": {
"type": "postgres",
"host": "127.0.0.1",
"port": 5432,
"user": "postgres",
"password": "secret",
"database": "myapp",
"schema": "public",
"overrideTypes": {
"jsonb": "z.record(z.string())",
"uuid": "z.string().uuid()"
}
}
}
{
"origin": {
"type": "sqlite",
"path": "./myapp.db",
"overrideTypes": {
"json": "z.record(z.string())",
"text": "z.string().max(1000)"
}
}
}
{
"origin": {
"type": "prisma",
"path": "./schema.prisma",
"overrideTypes": {
"Json": "z.record(z.string())",
"String": "z.string().min(1)"
}
}
}
You can use the @zod
comment to override the Zod type for a specific column. This is useful when you want to add custom validation or transformation to a field.
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL COMMENT '@zod(z.string().min(10).max(255))',
`email` varchar(255) NOT NULL COMMENT '@zod(z.string().email())',
PRIMARY KEY (`id`)
);
This will generate:
export const user = z.object({
id: z.number().nonnegative(),
name: z.string().min(10).max(255),
email: z.string().email(),
})
You can use the @ts
comment to override the TypeScript type for a specific column. This is useful when you want to specify a more precise type for a field.
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`metadata` json NOT NULL COMMENT '@ts(Record<string, unknown>)',
`settings` json NOT NULL COMMENT '@ts(UserSettings)',
PRIMARY KEY (`id`)
);
This will generate:
export interface User {
id: number;
metadata: Record<string, unknown>;
settings: UserSettings;
}
You can use complex TypeScript types in the @ts
comment:
CREATE TABLE `product` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`variants` json NOT NULL COMMENT '@ts(Array<{ id: string; price: number; stock: number }>)',
PRIMARY KEY (`id`)
);
This will generate:
export interface Product {
id: number;
variants: Array<{ id: string; price: number; stock: number }>;
}