A recursive JSON-to-relational conversion engine written in Python that analyzes semi-structured JSON data at runtime, infers a relational schema, generates PostgreSQL tables, creates primary/foreign-key relationships and migrates the original records automatically.
The project also includes a desktop Tkinter data explorer with a JSON hierarchy viewer, generated-table browser, SQL terminal, DataGrid and real-time migration logs.
The engine was originally developed as a university programming laboratory project and later refactored for improved security, schema robustness, dynamic SQL safety, testing and portfolio quality.
The objective of this project is to transform nested JSON structures into relational PostgreSQL schemas without requiring the target tables to be manually defined beforehand.
Given a JSON document, the engine dynamically performs the following transformation:
Nested / Semi-Structured JSON
↓
Recursive Structure Analysis
↓
Identifier Normalization
↓
Schema & Type Inference
↓
Nested Objects → Flattened Columns
Arrays → Child Tables
↓
Generated UUID Primary Keys
↓
Generated Parent / Child Foreign Keys
↓
Dynamic CREATE TABLE
↓
Parameterized INSERT
↓
FOREIGN KEY Constraints
↓
PostgreSQL Relational Model
| Capability | Implementation |
|---|---|
| Nested JSON parsing | Recursive Python processing |
| Nested objects | Flattened into relational columns |
| Arrays | Converted into child tables |
| Multi-level arrays | Recursive child-of-child table generation |
| Primary keys | UUID-based generated identifiers |
| Foreign keys | Generated parent-child relationships |
| Type inference | BOOLEAN, BIGINT, NUMERIC, TEXT |
| Mixed numeric values | Promoted to compatible PostgreSQL types |
| Identifier safety | Normalized names + sql.Identifier() |
| Inserts | Parameterized with psycopg2 placeholders |
| PostgreSQL identifier limit | Deterministic 63-character handling |
| Database reset | Explicit DROP confirmation through GUI |
| Configuration | .env-based credentials |
| Data exploration | Tkinter SQL terminal and DataGrid |
| Validation | Automated pytest unit tests |
flowchart LR
JSON["JSON File"]
GUI["Tkinter GUI<br/>JSON Tree & SQL Explorer"]
ENGINE["Conversion Engine"]
PARSE["Recursive Parsing"]
SCHEMA["Schema Inference"]
SQLGEN["Dynamic SQL Generation"]
PG[("PostgreSQL")]
GRID["SQL Terminal<br/>DataGrid"]
JSON --> GUI
GUI --> ENGINE
ENGINE --> PARSE
PARSE --> SCHEMA
SCHEMA --> SQLGEN
SQLGEN --> PG
PG --> GRID
GRID --> GUI
The user interface and conversion engine are intentionally separated.
gui.py
↓
User interaction, JSON tree, SQL terminal, DataGrid
parser_engine.py
↓
Parsing, schema inference, relational decomposition,
SQL generation and migration
config.py
↓
Environment-based PostgreSQL configuration
Nested objects are flattened into relational columns.
Input:
{
"shipping address": {
"city": "Kocaeli",
"postal-code": "41000"
}
}Generated columns:
shipping_address_city
shipping_address_postal_code
Arrays are represented as separate child tables.
Input:
{
"orders": [
{
"order id": 1001,
"total": 249.9
},
{
"order id": 1002,
"total": 99.5
}
]
}Conceptual relational result:
parent_table
│
└── id_pk
│
▼
parent_table_orders
├── id_pk
├── parent_table_id ← Foreign Key
├── order_id
└── total
The recursion continues for arrays inside arrays.
Example:
Customer
↓
Orders[]
↓
Items[]
becomes:
customer
│
▼
customer_orders
│
▼
customer_orders_items
Each level receives its own generated UUID primary key and explicit parent reference.
Every generated relational record receives a UUID-based primary key:
id_pk
When an array becomes a child table, the parent's generated UUID is copied into the child as a foreign-key column.
Conceptually:
Parent
├── id_pk = UUID-A
│
└── Child
├── id_pk = UUID-B
└── parent_id = UUID-A
After data insertion, the engine generates PostgreSQL foreign-key constraints with:
ON DELETE CASCADEPython values are mapped to PostgreSQL types at runtime.
| Python Value | PostgreSQL Type |
|---|---|
bool |
BOOLEAN |
int |
BIGINT |
float |
NUMERIC |
str |
TEXT |
None |
Resolved from other observed values |
When a field contains compatible mixed numerical types:
BIGINT + NUMERIC
the resulting PostgreSQL type becomes:
NUMERIC
If incompatible values appear in the same field, the engine falls back to:
TEXT
to reduce the risk of data loss.
Dynamic SQL is required because table and column names are discovered only after the JSON document is analyzed.
Untrusted identifiers are therefore not concatenated directly into SQL statements.
Instead, the project uses:
sql.Identifier(...)for PostgreSQL identifiers and:
sql.Placeholder()for inserted values.
For example, JSON keys such as:
customer data
postal-code
order id
select
are safely processed instead of being inserted directly into SQL strings.
JSON field names are normalized before being used as relational identifiers.
Examples:
"Customer Data" → customer_data
"postal-code" → postal_code
"order id" → order_id
"123-value" → field_123_value
Long identifiers are also handled deterministically to respect PostgreSQL's identifier length limit.
This is especially important when deeply nested JSON paths produce long generated table names.
flowchart TD
LOAD["Load JSON"]
ANALYZE["Recursive Analysis"]
MEMORY["Build Schema & Record Metadata"]
CREATE["1. CREATE TABLE"]
INSERT["2. Parameterized INSERT"]
FK["3. ADD FOREIGN KEYS"]
COMMIT["Transaction Commit"]
LOAD --> ANALYZE
ANALYZE --> MEMORY
MEMORY --> CREATE
CREATE --> INSERT
INSERT --> FK
FK --> COMMIT
The database migration is executed inside a transaction.
If migration fails:
Exception
↓
ROLLBACK
↓
Database changes are not committed
The Tkinter interface provides both JSON-side and SQL-side inspection.
It includes a hierarchical JSON tree, database table selector, automatically generated SELECT statements, custom SQL terminal, relational DataGrid and migration logs.
The screenshot demonstrates a nested structure containing:
customer data
↓
orders[]
↓
items[]
converted into the generated PostgreSQL table:
json_customer_data_orders_items
with the original product records available directly through the DataGrid.
The parser/schema layer includes automated unit tests using pytest.
The tests validate behavior such as identifier normalization, PostgreSQL identifier-length handling, deterministic long-name generation, type inference, type reconciliation, nested object flattening, nested array decomposition, parent/child foreign-key metadata, primitive-array handling, state reset behavior and empty-migration protection.
Run the tests with:
python -m pytest -vThe unit tests focus primarily on the conversion engine's in-memory schema-generation logic and therefore do not require a live PostgreSQL instance for every test.
The repository includes sample datasets for experimenting with the conversion engine:
examples/
├── european_countries.json
└── nested_customer_orders.json
nested_customer_orders.json intentionally contains nested objects, nested arrays, spaces in field names, hyphenated fields, booleans, integers, floating-point values and SQL-keyword-like names to exercise the engine's robustness.
Dynamic-JSON-to-PostgreSQL-Engine/
│
├── src/
│ ├── config.py
│ ├── gui.py
│ └── parser_engine.py
│
├── examples/
│ ├── european_countries.json
│ └── nested_customer_orders.json
│
├── tests/
│ └── test_parser_engine.py
│
├── docs/
│ └── images/
│ └── converter-overview.png
│
├── .env.example
├── .gitignore
├── requirements.txt
├── requirements-dev.txt
└── README.md
| Category | Technology |
|---|---|
| Language | Python |
| Database | PostgreSQL |
| PostgreSQL Driver | psycopg2 |
| GUI | Tkinter |
| Data Format | JSON |
| Testing | pytest |
| Configuration | python-dotenv |
| Database Modeling | Dynamic relational schema generation |
| Version Control | Git & GitHub |
Install:
Python
PostgreSQL
Git
git clone https://github.com/Avdatek5003/Dynamic-JSON-to-PostgreSQL-Engine.git
cd Dynamic-JSON-to-PostgreSQL-EngineWindows PowerShell:
python -m venv .venv
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\.venv\Scripts\Activate.ps1Linux / macOS:
python -m venv .venv
source .venv/bin/activateApplication dependencies:
python -m pip install -r requirements.txtFor development and testing:
python -m pip install -r requirements-dev.txtCreate a PostgreSQL database dedicated to the project.
Then copy:
.env.example
to:
.env
Windows PowerShell:
Copy-Item .env.example .envExample configuration:
DB_NAME=prolab_db
DB_USER=postgres
DB_PASSWORD=your_password
DB_HOST=localhost
DB_PORT=5432Real credentials must never be committed to source control.
python src/gui.pyThen:
1. Select a JSON file
↓
2. Reset the dedicated database schema if required
↓
3. Convert JSON to SQL
↓
4. Explore generated tables
The GUI includes a development-oriented reset operation that executes:
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;This deletes all objects stored in the target database's public schema.
For that reason, the application requires explicit confirmation before the operation is executed.
Use a dedicated project/test database. Do not point this functionality at a database containing important data.
Database credentials are loaded from environment variables rather than being stored directly in the Python source code.
.env
↓
config.py
↓
PostgreSQL connection
The real .env file is ignored by Git.
The public repository contains only:
.env.example
with placeholder values.
The project demonstrates practical use of:
Recursive algorithms
Semi-structured data processing
JSON parsing
Schema inference
Relational decomposition
Dynamic SQL generation
Parameterized SQL
Primary / foreign keys
UUID identifiers
PostgreSQL transactions
Type inference
Identifier normalization
Environment configuration
Desktop data exploration
Automated unit testing
Simple nested objects map naturally to prefixed relational columns and avoid unnecessary table proliferation.
Arrays represent repeated values or repeated entities. Child tables preserve each element as an independent relational record and allow parent-child relationships to be represented explicitly.
JSON objects may not contain reliable globally unique identifiers. Generated UUID values provide stable relational keys without depending on input-specific ID conventions.
The engine first creates the relational structure and inserts generated records, then adds the integrity constraints after all referenced records exist.
This project is primarily an educational schema-conversion engine rather than a production JSON database migration framework.
Current limitations include lack of schema evolution for repeated migrations into already-populated heterogeneous schemas, no automatic indexing strategy beyond generated relational keys, no streaming ingestion, basic type inference compared with production schema registries, no conflict-resolution policy for semantically different JSON keys that normalize to the same identifier, and a destructive database-reset utility intended strictly for a dedicated development database.
Future extensions could include schema evolution, automatic indexes, JSON Schema support, PostgreSQL JSONB comparison mode, collision detection for normalized identifiers, integration tests with disposable PostgreSQL containers, batch processing of multiple JSON files, CLI support, exportable migration reports and Docker-based reproducible database environments.
This project originated as a university programming laboratory project focused on converting NoSQL-style JSON structures into relational database representations.
The repository was later refactored to improve:
Security
Dynamic SQL safety
Configuration management
Identifier handling
Recursive relation tracking
Type inference
Error handling
Automated testing
Repository structure
while preserving the original recursive conversion-engine concept.
Ahmet Avdatek
Computer Engineering student focused on Data Engineering, database systems and data-intensive applications.
