TYTX vs Alternatives
How TYTX compares to other approaches for handling types across the wire.
The Problem
When sending data between Python and JavaScript, you need to handle types that JSON doesn’t support natively: Decimal, date, datetime, time. Here’s how different solutions approach this.
Comparison Table
Approach |
Schema Required |
Setup Complexity |
Runtime Overhead |
Type Safety |
|---|---|---|---|---|
TYTX |
No |
Minimal |
Low |
✅ Automatic |
Manual Conversion |
No |
None |
Low |
❌ Error-prone |
JSON Schema |
Yes |
Medium |
Medium |
⚠️ Validation only |
Protocol Buffers |
Yes |
High |
Low |
✅ Compile-time |
GraphQL |
Yes |
High |
Medium |
✅ Schema-based |
TypeBox/Zod |
Yes |
Medium |
Medium |
✅ Runtime |
Detailed Comparison
Manual Conversion (The Default)
What most applications do today:
# Server
return {
"price": str(price), # Decimal → string
"date": date.isoformat(), # date → string
}
// Client
const data = await response.json();
const price = new Decimal(data.price); // string → Decimal
const date = new Date(data.date); // string → Date
Problems:
Conversion code scattered everywhere
Easy to forget conversions
No guarantee client/server agree on format
Breaks when format changes
TYTX eliminates this entirely - types are preserved automatically.
JSON Schema
JSON Schema validates structure but doesn’t transport types:
{
"type": "object",
"properties": {
"price": { "type": "string", "pattern": "^\\d+\\.\\d{2}$" },
"date": { "type": "string", "format": "date" }
}
}
Limitations:
Only validates, doesn’t convert
Schema must be maintained separately
Client still receives strings
No runtime type conversion
TYTX advantage: Types are converted automatically, no schema needed.
Protocol Buffers / gRPC
Strongly typed with schema compilation:
message Order {
string price = 1; // No native Decimal
google.protobuf.Timestamp date = 2;
}
Trade-offs:
✅ Excellent type safety
✅ Efficient binary format
❌ Requires schema definition
❌ Requires code generation
❌ No native Decimal support
❌ Complex setup for web clients
When to use: High-performance microservices, mobile apps.
TYTX advantage: Works with standard JSON/HTTP, no build step, native Decimal support.
GraphQL
Type system with query language:
type Order {
price: String! # Custom scalar needed for Decimal
date: Date!
}
Trade-offs:
✅ Strong type system
✅ Client specifies what it needs
❌ Significant infrastructure
❌ Custom scalars for Decimal/Date
❌ Learning curve
When to use: Complex APIs with many clients needing different data shapes.
TYTX advantage: Zero infrastructure, works with existing REST APIs.
TypeBox / Zod (Runtime Validation)
TypeScript-first schema validation:
const OrderSchema = Type.Object({
price: Type.String(), // Still a string
date: Type.String(), // Still a string
});
Limitations:
Validates but doesn’t convert
TypeScript-only
Schema duplication between client/server
Manual conversion still needed
TYTX advantage: Actual type conversion, works across Python/JS.
When to Use TYTX
TYTX is ideal when you need:
Transparent type handling without conversion code
Decimal precision for financial data
Date/time preservation across client/server
Minimal setup - just use
asgi_data/wsgi_dataStandard HTTP/JSON - no special infrastructure
Transport flexibility - switch to MessagePack for better performance with a single parameter change
When to Consider Alternatives
Protocol Buffers: Extreme performance requirements, microservices
GraphQL: Complex data requirements, multiple client types
Manual conversion: Simple apps with few type conversions
Migration Path
TYTX works alongside existing solutions:
# Gradually adopt TYTX
@app.post("/api/v1/order") # Old endpoint - manual conversion
async def create_order_v1(request):
...
@app.post("/api/v2/order") # New endpoint - TYTX
async def create_order_v2(request):
data = request.scope["tytx"]["body"] # Types already converted
...
You can migrate endpoint by endpoint without breaking existing clients.
Real-World Comparison: Before and After
Scenario 1: Form with Many Typed Fields
A typical business form with mixed types: dates, decimals, timestamps, booleans.
❌ Without TYTX (Traditional Approach)
Server (Python):
@app.post("/api/save-contract")
async def save_contract(request: Request):
data = await request.json()
# Manual conversion of EVERY field
contract = Contract(
amount=Decimal(data["amount"]), # string → Decimal
start_date=date.fromisoformat(data["start_date"]), # string → date
end_date=date.fromisoformat(data["end_date"]),
created_at=datetime.fromisoformat(data["created_at"]),
hourly_rate=Decimal(data["hourly_rate"]),
discount=Decimal(data["discount"]) if data.get("discount") else None,
is_active=data["is_active"], # OK - native JSON
renewal_time=time.fromisoformat(data["renewal_time"]) if data.get("renewal_time") else None,
)
saved = await db.save(contract)
# Manual conversion back for response
return {
"id": saved.id,
"amount": str(saved.amount), # Decimal → string
"start_date": saved.start_date.isoformat(), # date → string
"end_date": saved.end_date.isoformat(),
"created_at": saved.created_at.isoformat(),
"hourly_rate": str(saved.hourly_rate),
"discount": str(saved.discount) if saved.discount else None,
"is_active": saved.is_active,
"renewal_time": saved.renewal_time.isoformat() if saved.renewal_time else None,
}
Client (JavaScript):
async function saveContract(formData) {
// Manual conversion before sending
const payload = {
amount: formData.amount.toString(), // Decimal → string
start_date: formData.startDate.toISOString().slice(0, 10), // Date → string
end_date: formData.endDate.toISOString().slice(0, 10),
created_at: formData.createdAt.toISOString(),
hourly_rate: formData.hourlyRate.toString(),
discount: formData.discount?.toString() ?? null,
is_active: formData.isActive,
renewal_time: formData.renewalTime
? `${formData.renewalTime.getUTCHours().toString().padStart(2,'0')}:${formData.renewalTime.getUTCMinutes().toString().padStart(2,'0')}:00`
: null,
};
const response = await fetch('/api/save-contract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
// Manual conversion after receiving
return {
id: result.id,
amount: new Decimal(result.amount), // string → Decimal
startDate: new Date(result.start_date), // string → Date
endDate: new Date(result.end_date),
createdAt: new Date(result.created_at),
hourlyRate: new Decimal(result.hourly_rate),
discount: result.discount ? new Decimal(result.discount) : null,
isActive: result.is_active,
renewalTime: result.renewal_time ? parseTime(result.renewal_time) : null,
};
}
Problems:
30+ lines of conversion code
Easy to forget a field or use wrong format
Time parsing is particularly error-prone
Changes require updates in 4 places (server in, server out, client in, client out)
✅ With TYTX
Server (Python):
@app.post("/api/save-contract")
async def save_contract(request: Request):
data = request.scope["tytx"]["body"]
# All types are already correct!
contract = Contract(
amount=data["amount"], # Already Decimal
start_date=data["start_date"], # Already date
end_date=data["end_date"],
created_at=data["created_at"], # Already datetime
hourly_rate=data["hourly_rate"],
discount=data.get("discount"),
is_active=data["is_active"],
renewal_time=data.get("renewal_time"), # Already time
)
saved = await db.save(contract)
# Just return - middleware handles encoding
return {
"id": saved.id,
"amount": saved.amount,
"start_date": saved.start_date,
"end_date": saved.end_date,
"created_at": saved.created_at,
"hourly_rate": saved.hourly_rate,
"discount": saved.discount,
"is_active": saved.is_active,
"renewal_time": saved.renewal_time,
}
Client (JavaScript):
async function saveContract(formData) {
// Send directly - fetchTytx handles encoding
const result = await fetchTytx('/api/save-contract', {
method: 'POST',
body: {
amount: formData.amount, // Decimal
start_date: formData.startDate, // Date
end_date: formData.endDate,
created_at: formData.createdAt, // Date
hourly_rate: formData.hourlyRate,
discount: formData.discount,
is_active: formData.isActive,
renewal_time: formData.renewalTime,
}
});
// Use directly - all types are correct
return result;
}
Result: Zero conversion code. Types flow naturally.
Scenario 2: Excel-like Grid with Bulk Data
Loading and saving a spreadsheet-like grid with hundreds of rows of financial data.
❌ Without TYTX
Server response:
@app.get("/api/transactions")
async def get_transactions():
rows = await db.fetch_all("SELECT * FROM transactions LIMIT 500")
# Convert EVERY cell in EVERY row
return {
"rows": [
{
"id": row.id,
"date": row.date.isoformat(),
"amount": str(row.amount),
"tax": str(row.tax),
"total": str(row.total),
"due_date": row.due_date.isoformat(),
"paid_at": row.paid_at.isoformat() if row.paid_at else None,
"rate": str(row.rate),
"quantity": row.quantity,
"unit_price": str(row.unit_price),
"discount_pct": str(row.discount_pct),
}
for row in rows
]
}
Client loading:
async function loadGrid() {
const response = await fetch('/api/transactions');
const data = await response.json();
// Convert every cell back
return data.rows.map(row => ({
id: row.id,
date: new Date(row.date),
amount: new Decimal(row.amount),
tax: new Decimal(row.tax),
total: new Decimal(row.total),
dueDate: new Date(row.due_date),
paidAt: row.paid_at ? new Date(row.paid_at) : null,
rate: new Decimal(row.rate),
quantity: row.quantity,
unitPrice: new Decimal(row.unit_price),
discountPct: new Decimal(row.discount_pct),
}));
}
Client saving (user edited 50 rows):
async function saveChanges(modifiedRows) {
const payload = modifiedRows.map(row => ({
id: row.id,
date: row.date.toISOString().slice(0, 10),
amount: row.amount.toString(),
tax: row.tax.toString(),
total: row.total.toString(),
due_date: row.dueDate.toISOString().slice(0, 10),
paid_at: row.paidAt?.toISOString() ?? null,
rate: row.rate.toString(),
quantity: row.quantity,
unit_price: row.unitPrice.toString(),
discount_pct: row.discountPct.toString(),
}));
await fetch('/api/transactions', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rows: payload })
});
}
Server receiving edits:
@app.put("/api/transactions")
async def update_transactions(request: Request):
data = await request.json()
for row in data["rows"]:
await db.execute(
"UPDATE transactions SET date=?, amount=?, tax=?, ... WHERE id=?",
date.fromisoformat(row["date"]),
Decimal(row["amount"]),
Decimal(row["tax"]),
Decimal(row["total"]),
date.fromisoformat(row["due_date"]),
datetime.fromisoformat(row["paid_at"]) if row["paid_at"] else None,
Decimal(row["rate"]),
row["quantity"],
Decimal(row["unit_price"]),
Decimal(row["discount_pct"]),
row["id"],
)
Total: ~80 lines of conversion code for one grid.
✅ With TYTX
Server:
@app.get("/api/transactions")
async def get_transactions():
rows = await db.fetch_all("SELECT * FROM transactions LIMIT 500")
return {"rows": [dict(row) for row in rows]} # Types preserved automatically
@app.put("/api/transactions")
async def update_transactions(request: Request):
data = await asgi_data(request.scope, request.receive)
for row in data["body"]["rows"]:
await db.execute(
"UPDATE transactions SET date=?, amount=?, ... WHERE id=?",
row["date"], row["amount"], row["tax"], row["total"],
row["due_date"], row["paid_at"], row["rate"],
row["quantity"], row["unit_price"], row["discount_pct"],
row["id"],
)
Client:
async function loadGrid() {
const data = await fetchTytx('/api/transactions');
return data.rows; // All types correct
}
async function saveChanges(modifiedRows) {
await fetchTytx('/api/transactions', {
method: 'PUT',
body: { rows: modifiedRows } // Types preserved
});
}
Total: Zero conversion code.
Scenario 3: Query Parameters with Typed Filters
Filtering a list with date ranges and price thresholds.
❌ Without TYTX
Client:
const url = new URL('/api/orders');
url.searchParams.set('start_date', startDate.toISOString().slice(0, 10));
url.searchParams.set('end_date', endDate.toISOString().slice(0, 10));
url.searchParams.set('min_price', minPrice.toString());
url.searchParams.set('max_price', maxPrice.toString());
url.searchParams.set('created_after', createdAfter.toISOString());
const response = await fetch(url);
Server:
@app.get("/api/orders")
async def list_orders(request: Request):
params = request.query_params
start_date = date.fromisoformat(params["start_date"])
end_date = date.fromisoformat(params["end_date"])
min_price = Decimal(params["min_price"])
max_price = Decimal(params["max_price"])
created_after = datetime.fromisoformat(params["created_after"])
# Use in query...
✅ With TYTX
Client (URL built manually with TYTX suffixes):
/api/orders?start_date=2025-01-01::D&end_date=2025-12-31::D&min_price=100.00::N&max_price=500.00::N
Server:
@app.get("/api/orders")
async def list_orders(request: Request):
data = await asgi_data(request.scope, request.receive)
params = data["query"]
# Already typed!
start_date = params["start_date"] # date
end_date = params["end_date"] # date
min_price = params["min_price"] # Decimal
max_price = params["max_price"] # Decimal
Summary: Lines of Code Comparison
Scenario |
Without TYTX |
With TYTX |
Savings |
|---|---|---|---|
Form (8 typed fields) |
~60 lines |
0 lines |
100% |
Grid (500 rows × 10 cols) |
~80 lines |
0 lines |
100% |
Query filters (5 params) |
~15 lines |
0 lines |
100% |
More importantly: Zero bugs from forgotten conversions, wrong formats, or timezone issues.