Schema Diff
Last updated: March 2026
The Schema Diff API lets you compare two ER diagrams — an old version and a new version — and receive a structured JSON list of every schema change. It supports Mermaid and PlantUML, including cross-format comparisons. The endpoint is free and requires no authentication.
Use Schema Diff to understand what changed between two diagram revisions, power code review tools, or build change-log generators. If you need the actual SQL ALTER TABLE / CREATE TABLE statements to apply the diff to a live database, see POST /v1/convert/migration.
Interactive UI
The easiest way to use Schema Diff is the Diff page. Paste your old diagram on the left and your new diagram on the right, select the format for each panel, and click Compare Schemas. Results are colour-coded immediately in the browser.
- Green (+) — additions: new tables, columns, indexes, foreign keys
- Red (−) — deletions: dropped tables, columns, indexes, foreign keys
- Yellow (~) — modifications: altered column type, nullability, or default
A Destructive badge marks changes that could cause data loss — dropped tables or columns, type changes, and NOT NULL additions without a default value.
POST /v1/diff
Compare two diagrams and receive a structured JSON diff. No API key required.
POST https://diagram2code.com/v1/diff Content-Type: application/json
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
oldDiagramType | string | Yes | Format of the baseline diagram: "mermaid" or "plantuml" |
oldDiagram | string | Yes | The baseline (old) diagram text |
newDiagramType | string | Yes | Format of the updated diagram: "mermaid" or "plantuml" |
newDiagram | string | Yes | The updated (new) diagram text |
{
"oldDiagramType": "mermaid",
"oldDiagram": "erDiagram\n USER {\n int id PK\n string name\n }\n",
"newDiagramType": "mermaid",
"newDiagram": "erDiagram\n USER {\n int id PK\n string name\n string email\n }\n ORDER {\n int id PK\n int user_id FK\n decimal total\n }\n USER ||--o{ ORDER : places\n"
}Response Fields
A successful request returns HTTP 200 OK with the following JSON body.
| Field | Type | Description |
|---|---|---|
changes | array | Ordered list of schema changes. Empty when the two diagrams are schema-equivalent. |
changes[].op | string | The change operation — see Change Operations. |
changes[].table | string | The table affected by this change. |
changes[].column | string | The column affected (omitted for table-level operations). |
changes[].description | string | Human-readable description of the change. |
changes[].destructive | boolean | true when the change may cause data loss. |
summary.total | integer | Total number of changes. |
summary.additions | integer | Number of additive changes (CREATE_TABLE, ADD_COLUMN, ADD_INDEX, ADD_FK). |
summary.deletions | integer | Number of removal changes (DROP_TABLE, DROP_COLUMN, DROP_INDEX, DROP_FK). |
summary.modifications | integer | Number of modifications (ALTER_COLUMN). |
summary.destructiveCount | integer | Number of changes where destructive is true. |
{
"changes": [
{
"op": "ADD_COLUMN",
"table": "USER",
"column": "email",
"description": "Added column \"email\" to table \"USER\"",
"destructive": false
},
{
"op": "CREATE_TABLE",
"table": "ORDER",
"description": "Created table \"ORDER\"",
"destructive": false
},
{
"op": "ADD_FK",
"table": "ORDER",
"description": "Added foreign key on \"ORDER\" referencing \"USER\"",
"destructive": false
}
],
"summary": {
"total": 3,
"additions": 3,
"deletions": 0,
"modifications": 0,
"destructiveCount": 0
}
}Change Operations
The op field in each change entry is one of the following constants.
| Op | Kind | Destructive? | Description |
|---|---|---|---|
CREATE_TABLE | addition | No | A new table was added in the new diagram |
DROP_TABLE | deletion | Always | A table present in the old diagram is missing from the new diagram |
ADD_COLUMN | addition | No | A new column was added to an existing table |
DROP_COLUMN | deletion | Always | A column was removed from an existing table |
ALTER_COLUMN | modification | Sometimes | A column's type, nullability, or default value changed. Destructive when the type changes, or when NOT NULL is added without a default value. |
ADD_INDEX | addition | No | A new index was added |
DROP_INDEX | deletion | No | An index was removed |
ADD_FK | addition | No | A foreign key relationship was added |
DROP_FK | deletion | No | A foreign key relationship was removed |
Curl Examples
1. Add a column
curl -s https://diagram2code.com/v1/diff \
-H 'Content-Type: application/json' \
-d '{
"oldDiagramType": "mermaid",
"oldDiagram": "erDiagram\n USER {\n int id PK\n string name\n }\n",
"newDiagramType": "mermaid",
"newDiagram": "erDiagram\n USER {\n int id PK\n string name\n string email\n }\n"
}'2. Add a table with a foreign key
curl -s https://diagram2code.com/v1/diff \
-H 'Content-Type: application/json' \
-d '{
"oldDiagramType": "mermaid",
"oldDiagram": "erDiagram\n USER { int id PK\n string name }\n",
"newDiagramType": "mermaid",
"newDiagram": "erDiagram\n USER { int id PK\n string name }\n ORDER { int id PK\n int user_id FK }\n USER ||--o{ ORDER : places\n"
}'3. Drop a column (destructive)
curl -s https://diagram2code.com/v1/diff \
-H 'Content-Type: application/json' \
-d '{
"oldDiagramType": "mermaid",
"oldDiagram": "erDiagram\n USER {\n int id PK\n string name\n string legacy_field\n }\n",
"newDiagramType": "mermaid",
"newDiagram": "erDiagram\n USER {\n int id PK\n string name\n }\n"
}'4. Cross-format diff (Mermaid → PlantUML)
You can compare diagrams in different formats. The two diagrams are parsed independently then diffed on their normalised schema representations, so the source format is irrelevant.
curl -s https://diagram2code.com/v1/diff \
-H 'Content-Type: application/json' \
-d '{
"oldDiagramType": "mermaid",
"oldDiagram": "erDiagram\n USER { int id PK\n string name }\n",
"newDiagramType": "plantuml",
"newDiagram": "@startuml\nentity USER {\n * id : int <<PK>>\n name : string\n email : string\n}\n@enduml"
}'5. Identical diagrams
When the two diagrams describe the same schema (after normalisation), the response will have an empty changes array and all summary counts will be zero.
curl -s https://diagram2code.com/v1/diff \
-H 'Content-Type: application/json' \
-d '{
"oldDiagramType": "mermaid",
"oldDiagram": "erDiagram\n USER { int id PK }\n",
"newDiagramType": "mermaid",
"newDiagram": "erDiagram\n USER { int id PK }\n"
}'Destructive Changes
A change is marked "destructive": true when applying it to a live database could cause permanent data loss. The following operations are always destructive:
- DROP_TABLE — all rows in the table are permanently lost
- DROP_COLUMN — all values in the column are permanently lost
The following are conditionally destructive:
- ALTER_COLUMN type change — converting a column to a different data type may truncate or error on existing values (e.g.
string→int) - ALTER_COLUMN nullable → NOT NULL without a default — any existing
NULLvalues will cause the migration to fail; destructive because existing rows may need to be updated first
These are never destructive:
- ALTER_COLUMN NOT NULL → nullable — relaxing a constraint is always safe
- ALTER_COLUMN adding a default value — this only affects future inserts
- DROP_INDEX, DROP_FK — no data is lost when removing indexes or foreign key constraints
Use summary.destructiveCount to gate CI checks or PR comments when any destructive change is present.
Diff vs Migration SQL
Schema Diff (POST /v1/diff) returns a human-readable, machine-parseable description of what changed. It is free, requires no authentication, and produces no SQL.
Migration SQL (POST /v1/convert/migration) generates the actual ALTER TABLE, CREATE TABLE, and DROP statements needed to evolve a live database from the old schema to the new one. It is dialect-aware (PostgreSQL, MySQL, SQLite, Oracle) and requires a Developer or higher plan.
| POST /v1/diff | POST /v1/convert/migration | |
|---|---|---|
| Output | Structured JSON change list | SQL DDL migration script |
| Free tier | Yes — no API key required | No — Developer plan required |
| SQL dialect | Not applicable | postgres, mysql, sqlite, oracle |
| Warnings for destructive ops | Via destructive: true flag | Via SQL -- WARNING: comments |
| Cross-format | Yes | Yes |
Troubleshooting
INPUT_ERROR — "oldDiagram is required"
All four fields (oldDiagramType, oldDiagram, newDiagramType, newDiagram) must be present and non-empty. If any field is missing or blank the server returns HTTP 400 with "code": "INPUT_ERROR".
INPUT_ERROR — "unsupported diagram type"
The only accepted values for oldDiagramType and newDiagramType are "mermaid" and "plantuml" (lowercase). Any other value — including "graphviz", "dbml", or an empty string — returns a 400 error.
PARSE_ERROR — "expected erDiagram keyword"
Every Mermaid diagram must begin with the literal keyword erDiagram. If your diagram starts with a comment, a blank line, or a different Mermaid diagram type, the parser will reject it. Strip any leading whitespace or %% comment lines before sending.
PARSE_ERROR with PlantUML
PlantUML diagrams must start with @startuml and end with @enduml. Entity blocks use the entity keyword. Attribute markers like * (mandatory) and <<PK>> (primary key) are optional but must be well-formed if present. See PlantUML Syntax for the full grammar reference.
Changes array is empty but I made changes
Both schemas are normalised before diffing. Normalisation injects a synthetic primary key column if no PK is defined, and it may rename or deduplicate columns. If normalisation produces an equivalent schema from both diagrams, the diff will be empty. To verify, try the interactive Diff UI and inspect the rendered change list. If the diagrams are semantically identical after normalisation, the empty result is correct.
Cross-format diff shows unexpected DROP + CREATE instead of ADD_COLUMN
The diff algorithm is case-sensitive on table names. If your Mermaid diagram has USER (uppercase) and your PlantUML diagram has entity User (title case), they are treated as different tables. Use the same casing for table names across both diagrams to get accurate column-level diffs.