A TypeScript library that allows you to define data models using Zod with extended metadata (Primary Key, Foreign Key, Unique) and convert them into framework-agnostic Entity Definitions (ER models).
- 🔧 Zod Extensions: Add
.pk(),.unique(), and.ref()methods to Zod schemas - 🏗️ Entity & Struct Factories: Define entities and reusable struct types
- 🔄 Automatic Generation: Convert Zod schemas to entity definitions and relations
- 🔒 Type-Safe: Full TypeScript support with strict typing
- 📦 Framework Agnostic: Generate generic entity definitions usable by any framework
- ✅ Validation: Built-in validation for entity nesting and reference integrity
Install the package from npm:
npm install @masaori/zod-to-entity-definitions zodzod is a peer dependency and both 3.25.x and 4.x are supported. CI runs the
full test suite against both, so either major may be used without a source change.
import { z } from 'zod';
import {
entity,
struct,
generateEntities,
generateRelations,
} from '@masaori/zod-to-entity-definitions';
// 1. Define a Struct (reusable component)
const Address = struct({
name: 'AddressStruct',
description: 'Common address',
columns: {
city: z.string(),
street: z.string(),
},
});
// 2. Define Entities
const Company = entity({
name: 'Company',
columns: {
id: z.string().pk(),
name: z.string(),
address: Address, // Using struct is OK
},
});
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
email: z.string().email().unique(),
companyId: z.string().ref(Company), // Reference to Company entity
},
});
// 3. Generate Entity Definitions
const definitions = generateEntities([Company, User]);
// 4. Generate Relations
const relations = generateRelations(definitions);
console.log(JSON.stringify(definitions, null, 2));
console.log(JSON.stringify(relations, null, 2));📝 See the examples directory for complete working examples with generated JSON output.
The library extends Zod schemas with the following methods:
Marks a field as the Primary Key.
id: z.string().pk();Marks a field as Unique.
email: z.string().email().unique();Marks a field as a Foreign Key reference to another entity.
targetEntity: Must be a schema created withentity()targetColumn: Optional, defaults to "id"
companyId: z.string().ref(Company);
managerId: z.string().ref(User, 'userId');Wrapping a reference in z.array(...) marks it as an array (to-many) foreign key.
The generated ReferencedObject property and the corresponding referTos / referredBys
carry isArray: true; single (scalar) references omit the field, so existing output is
unchanged.
// any of these forms are recognized as an array reference
productIds: z.array(z.string().ref(Product));
productIds: z.string().ref(Product).array();
productIds: z.array(z.string()).ref(Product);Behavior change: array references were already accepted by earlier versions, but were emitted without
isArray— indistinguishable from a scalar reference (the to-many information was silently dropped). From this version they carryisArray: true. Scalar reference output is unchanged. If you rely on exact-equality snapshots of generated output for schemas that use array references, those snapshots will need updating.
Creates an entity schema with metadata.
type EntityConfig<T extends z.ZodRawShape> = {
name: string;
description?: string;
columns: T;
// Composite (multi-column) unique constraints. Column names are type-checked against `columns`.
uniques?: (keyof T & string)[][];
};Creates a struct schema for reusable components.
type StructConfig<T extends z.ZodRawShape> = {
name: string;
description?: string;
columns: T;
};Parses Zod schemas and returns an array of EntityDefinition.
Validation Rules:
- Entities cannot directly embed other entities (must use
.ref()) .ref()must point to valid entity schemas
const definitions = generateEntities([Company, User]);Analyzes entity definitions to construct relation maps.
const relations = generateRelations(definitions);type EntityDefinition = {
name: string;
description?: string;
properties: EntityPropertyDefinition[];
};Union type representing different property types:
EntityPropertyDefinitionPrimaryKey: Primary key fieldEntityPropertyDefinitionPrimitive: boolean, number, string, DateEntityPropertyDefinitionTypedStruct: Reference to a struct typeEntityPropertyDefinitionReferencedObject: Foreign key reference (carriesisArray?: truefor array / to-many references)
type EntityRelation = {
entityName: string;
referTos: EntityRelationReferTo[];
referredBys: EntityRelationReferredBy[];
};const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
nickname: z.string().optional(), // Optional field
bio: z.string().nullable(), // Nullable field
},
});const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
tags: z.array(z.string()),
scores: z.array(z.number()).optional(),
},
});const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
role: z.enum(['admin', 'user', 'guest']),
},
});Single-column uniqueness uses .unique() on the column. For multi-column (composite) uniqueness, declare it at the entity level with uniques. Column names are type-checked against columns.
const CustomerSegmentProductVariantOffering = entity({
name: 'CustomerSegmentProductVariantOffering',
columns: {
id: z.string().pk(),
productVariantId: z.string().ref(ProductVariant),
customerSegmentId: z.string().ref(CustomerSegment),
price: z.number(),
},
// (productVariantId, customerSegmentId) must be unique together
uniques: [['productVariantId', 'customerSegmentId']],
});The constraint is emitted on the generated EntityDefinition as compositeUniqueConstraints:
{
"name": "CustomerSegmentProductVariantOffering",
"properties": [ /* ... */ ],
"compositeUniqueConstraints": [["productVariantId", "customerSegmentId"]]
}- Multiple constraints are supported:
uniques: [['a', 'b'], ['c', 'd']]. - Each group must list at least 2 distinct, existing columns (otherwise
generateEntitiesthrows). Use.unique()for single-column uniqueness. - When no
uniquesare declared,compositeUniqueConstraintsis omitted from the output (existing consumers are unaffected).
const Department = entity({
name: 'Department',
columns: {
id: z.string().pk(),
name: z.string(),
},
});
const Employee = entity({
name: 'Employee',
columns: {
id: z.string().pk(),
name: z.string(),
departmentId: z.string().ref(Department),
},
});
const Project = entity({
name: 'Project',
columns: {
id: z.string().pk(),
name: z.string(),
leadId: z.string().ref(Employee),
departmentId: z.string().ref(Department),
},
});const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
company: Company, // ❌ Error: Direct entity embedding not allowed
},
});const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
companyId: z.string().ref(Company), // ✅ Correct: Use .ref()
},
});const Address = struct({
name: 'Address',
columns: { city: z.string() },
});
const User = entity({
name: 'User',
columns: {
id: z.string().pk(),
addressId: z.string().ref(Address), // ❌ Error: Can't reference struct
},
});# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Type checking
pnpm check-types
# Linting
pnpm lint
# Auto-fix lint issues
pnpm lint:fix
# Format code
pnpm format
# Build
pnpm buildThis project uses GitHub Actions for continuous integration and deployment.
-
Commit Message Validation (
.github/workflows/commit-check.yml)- Runs on all PRs
- Validates commit messages using Conventional Commits
- Ensures all commits are linear (no merge commits)
-
Lint and Test (
.github/workflows/lint-test.yml)- Runs on all PRs and pushes to main
- Executes linting, type checking, and tests
- Must pass before merging
-
Publish to GitHub Packages (
.github/workflows/publish.yml)- Manual workflow trigger only (workflow_dispatch)
- Automatically determines version bump using semantic versioning
- Publishes to GitHub Packages with automatic changelog generation
- Version bumps follow Conventional Commits:
feat:→ minor version bumpfix:,docs:,style:,refactor:,test:,build:,ci:,chore:→ patch version bumpBREAKING CHANGE:→ major version bump
Publishing is automated via GitHub Actions. To publish a new version:
-
Required tokens: No additional tokens needed!
- The workflow uses the built-in
GITHUB_TOKENwhich is automatically provided by GitHub Actions - The
GITHUB_TOKENhas the necessary permissions to:- Write to the repository (create tags, update files)
- Publish to GitHub Packages
- Create GitHub releases
- No manual token configuration required - it works out of the box
- The workflow uses the built-in
-
Trigger the publish workflow:
- Go to the Actions tab in GitHub
- Select "Publish to GitHub Packages" workflow
- Click "Run workflow" on the main branch
-
Automated process:
- The workflow will analyze commit messages since the last release
- Automatically determine the version bump (major/minor/patch)
- Update
package.jsonandCHANGELOG.md - Create a git tag and GitHub release
- Publish to GitHub Packages
All commits must follow the Conventional Commits specification:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Examples:
feat: add new entity validationfix: correct reference resolutiondocs: update README with examplesfeat!: remove deprecated API(breaking change)
The package includes a prepublishOnly script that automatically runs before publishing:
- Type checking (
pnpm check-types) - Linting (
pnpm lint) - Tests (
pnpm test) - Build (
pnpm build)
All these checks must pass before the package can be published.
MIT