I Kept Typing the Same Schema Twice. So I Built diagram2code.
There's a particular kind of tedium that doesn't feel like a problem until you've done it a hundred times. For me it was translating ER diagrams into database schemas by hand.
You know the ritual. You sketch the data model โ boxes, lines, a USER that places an ORDER. Everyone nods. Then you go and write the CREATE TABLE statements. Then you write the ORM models that mirror those tables. Then, three weeks later, someone adds a column to the diagram, and now there are three sources of truth: the picture in the wiki, the SQL in the migration, and the structs in the code. They agree on Monday. By Friday they don't.
The drift is never dramatic. It's a nullable that should've been NOT NULL. It's a foreign key that exists in the diagram but never made it into a migration. It's created_at in one place and createdAt in another. Small, subtle, and exactly the kind of thing that surfaces at 11pm when a query returns nothing and you can't figure out why.
I lived in that gap for years. And the thing that finally bothered me wasn't the typing โ it was that I was doing a translation job that a computer is much better at than I am.
The insight was boring, which is how I knew it was right
I almost built the obvious thing first: a Mermaid-to-PostgreSQL converter. One input, one output. A weekend project.
But I'd been burned by weekend projects that calcify into a wall of if statements. If I supported Mermaid and PlantUML, and PostgreSQL and MySQL, and then someone wanted GORM models, I'd be writing a converter for every pair of formats. Two diagram syntaxes times four SQL dialects times eight ORMs is not a weekend. It's a swamp.
So I stopped and drew a different picture. Instead of connecting inputs to outputs directly, what if everything passed through one shape in the middle?
That shape is an intermediate schema model. Every parser produces one. Every generator consumes one. A Schema is just Tables, each with Columns, Relationships, and Indexes โ relationships expressed as plain "table.column" references with a cardinality like one_to_many and OnDelete/OnUpdate behavior attached.
The payoff is the part I'm still a little smug about: parsers and generators never know about each other. Adding a new diagram format means writing only the parser. Adding a new output target means writing only the generator. No new pair-wise anything. The swamp never forms.
It's not a clever idea. It's a compiler idea, borrowed wholesale. But applying it here meant the project could grow without collapsing under its own combinations.
What it actually does today
I want to be specific and honest, because origin stories tend to drift into hype the same way schemas drift out of sync. Here's what's shipped right now.
In: Mermaid erDiagram and PlantUML entity diagrams.
Out: SQL DDL for PostgreSQL, MySQL, SQLite, and Oracle; ORM models for eight frameworks โ GORM, SQLAlchemy, TypeORM, Prisma, JPA, Django, Sequelize, and EF Core; plus a GraphQL schema and an OpenAPI 3.1 document.
You write this:
erDiagram
USER ||--o{ ORDER : places
USER {
int id PK
string email
string name
}
ORDER {
int id PK
int user_id FK
decimal total
datetime created_at
} And you get back real CREATE TABLE SQL in the dialect you asked for, with the foreign key wired up. Or GORM structs. Or a Prisma schema. Same diagram, your choice of target.
Diagram syntax can't express everything a real schema needs, so there's a directive system โ annotations you drop into the source as comments:
PRODUCT {
int id PK
%% ::CHECK(price > 0), ::NN
decimal price
%% ::DEFAULT(0)
int stock
} That gives you NOT NULL, defaults, check constraints, enums, named unique constraints, composite indexes with WHERE clauses, cascade rules โ the stuff that's the difference between a toy schema and one you'd actually deploy. The model also fills in gaps for you: it injects a synthetic id primary key when one is missing, infers many-to-many junction tables, and validates for dangling foreign keys and duplicate tables before it generates a line.
Two more pieces earn their keep in my own workflow. The first is a migration diff engine: hand it the old diagram and the new one, and it works out the ordered set of operations to get from one to the other โ this column added, that constraint dropped, this table renamed โ emitting migration SQL in all four dialects. Crucially, it doesn't treat every change as equal. Anything lossy โ dropping a column or a table, narrowing a column's type โ comes back tagged with an explicit warning, so a risky edit can't slip through looking routine.
The second piece is where that lands in practice: a GitHub Action I run on the repos themselves. Whenever a pull request edits a .mmd or .puml file, the action calls the hosted API and drops the regenerated SQL and a plain-English schema diff straight into the PR as a sticky comment โ so the reviewer reads the consequences of the diagram change without leaving the review. And if you'd rather not merely warn, you can have it block the merge outright on anything destructive:
- uses: diagram2code/diagram2code@v1
with:
database: postgres
fail-on-destructive: true That's the bit that finally closed the loop on my original 11pm problem. The diagram is the review artifact now. Drift surfaces in the pull request, while someone's still looking โ not three weeks later in production.
If you'd rather not touch a UI, the whole thing is a REST API:
curl -X POST https://diagram2code.com/api/v1/convert/sql \
-H "Content-Type: application/json" \
-d '{"diagramType":"mermaid","database":"postgres","diagram":"erDiagram\nUSER {\n int id PK\n}"}' You can use a good chunk of it without an account โ SQL for every dialect, GORM, the linter, the visual diff, and the live render preview are all free, up to a monthly limit.
Where it's going
A couple of things are on the roadmap that I want to name plainly as not yet shipped, because I'd rather under-promise.
The next big one is class-diagram-to-code: turning Mermaid classDiagram and PlantUML class diagrams into Go, Python, and TypeScript โ the same intermediate-model trick, pointed at code instead of schemas. After that, project scaffolding โ generating a runnable starting point rather than just files you paste in. Neither exists today. When they do, they'll ride the exact same pipeline, which is the whole reason I built the pipeline first.
If you've felt this itch too
If you've ever typed a schema twice and watched the two copies wander apart, give it a try at diagram2code.com. Paste a diagram, watch it render, pick a target. It's the tool I wish I'd had for the last decade of side projects.
I'm building it in the open and writing as I go. If the problem is familiar, follow along โ and tell me what your diagrams should turn into next.