@genrojs/tytx
v0.16.2
Published
Typed data interchange between Python and JavaScript over JSON, XML and MessagePack: Decimal, dates and custom types arrive with their type.
Maintainers
Readme
genro-tytx
A lightweight multi-transport typed data interchange system.
TYTX eliminates manual type conversions between Python and JavaScript, and makes switching to MessagePack for better performance as simple as changing a parameter.
You send a Decimal from Python, JavaScript receives a string. You convert it back. Every. Single. Time. TYTX fixes this—types flow automatically between Python and JavaScript, over JSON, XML, or MessagePack.
The Pain You Know
# Your Python API
return {"price": Decimal("99.99"), "due_date": date(2025, 1, 15)}// Your JavaScript client
const data = await response.json();
// data.price is "99.99" (string) - need to convert
// data.due_date is "2025-01-15" (string) - need to convert
const price = new Decimal(data.price); // Manual conversion
const dueDate = new Date(data.due_date); // Manual conversionThis leads to:
- Conversion code scattered everywhere
- Bugs when someone forgets to convert
- Financial calculations with floating-point errors
- Different date formats causing off-by-one-day bugs
The TYTX Solution
# Server - just return native types
return {"price": Decimal("99.99"), "due_date": date(2025, 1, 15)}// Client - types arrive ready to use
const data = await fetchTytx('/api/order');
data.price // → Decimal (not string)
data.due_date // → Date (not string)Zero conversion code. Types just work.
30-Second Demo
Python:
pip install genro-tytxfrom decimal import Decimal
from datetime import date
from genro_tytx import to_tytx, from_tytx
# Encode
data = {"price": Decimal("99.99"), "date": date(2025, 1, 15)}
encoded = to_tytx(data)
# '{"price": "99.99::N", "date": "2025-01-15::D"}::JS'
# Decode
decoded = from_tytx(encoded)
# {"price": Decimal("99.99"), "date": date(2025, 1, 15)}JavaScript:
npm install @genrojs/tytx # npm
bunx jsr add @genro/tytx # JSRimport { fetchTytx } from '@genro/tytx';
import Big from 'big.js';
const result = await fetchTytx('/api/invoice', {
body: { price: new Big('99.99'), date: new Date() }
});
// result.total → Big (ready to use)Untyped JSON codec
Besides the typed to_tytx / from_tytx API, TYTX exposes a plain JSON codec for the untyped path — no type suffixes, just fast JSON (orjson when available, stdlib otherwise):
from genro_tytx import json_dumps, json_loads
json_dumps({"a": 1}) # -> b'{"a":1}' (UTF-8 bytes)
json_loads('{"a": 1}') # accepts str or bytes -> {"a": 1}json_dumps returns UTF-8 bytes (ready for ASGI/WebSocket send), json_loads accepts str or bytes. Use these when you want plain JSON; use to_tytx / from_tytx when you want typed values to flow.
Custom types
register_type lets an external package teach TYTX a new type — a serialize hook (object → string) and a deserialize hook (string → object) under a custom suffix. The package registers at its own import time, so TYTX gains no dependency on it:
from genro_tytx import register_type, to_tytx, from_tytx
class Point:
def __init__(self, x, y): self.x, self.y = x, y
register_type(Point, "PT", lambda p: f"{p.x},{p.y}",
lambda s: Point(*map(int, s.split(","))))
to_tytx([1, Point(5, 6), "k"]) # '[1,"5,6::PT","k"]::JS'
from_tytx('5,6::PT') # Point(5, 6)The custom type flows like any built-in scalar, including nested inside dicts and lists. The exact type is matched first; an unregistered subclass of a registered type travels under that type's code, written by its own serializer (genro-builders' SourceBag travels as genro-bag's X). Built-in types keep the exact-type rule. A class cannot be registered under a code another class owns. A code is one or more uppercase ASCII letters, any length; registration refuses anything else. Re-registering the same class replaces its hooks; reusing a suffix owned by a different type raises an error. An unknown code on the receiving side is not an error: the string comes back untouched. The same semantics apply to the JavaScript client (registerType / registerClass). The rules and the codes reserved by consumers are in spec/TYTX-SPEC.md §2.5.
When the type owns its serialization, register_class reads the hooks from the class itself — usable as a decorator. It needs __tytx_suffix__, an instance to_tytx() and a from_tytx classmethod (from_tytx must be a classmethod: decoding starts from the suffix and rebuilds the instance from scratch):
from genro_tytx import register_class
@register_class
class Point:
__tytx_suffix__ = "PT"
def __init__(self, x, y): self.x, self.y = x, y
def to_tytx(self): return f"{self.x},{self.y}"
@classmethod
def from_tytx(cls, s): return cls(*map(int, s.split(",")))The concrete class of a subclass is the type's own business. TYTX keeps one subtype dictionary per code and never reads it: set_subtype_dict(suffix, dict) replaces it, get_subtype_dict(suffix) returns it ({} if none was set). JavaScript: setSubtypeDict / getSubtypeDict. The type's to_tytx/from_tytx use it; whoever adds a subclass reads the dictionary, adds its entries and sets it again. genro-bag maps symbolic names to Bag classes this way.
Installation
# Python
pip install genro-tytx
# JavaScript/TypeScript, from npm
npm install @genrojs/tytx
# JavaScript/TypeScript, from JSR
bunx jsr add @genro/tytx
# With npm: npx jsr add @genro/tytxJavaScript includes Decimal, Big, MessagePack and XML dependencies. No separate codec installation is needed. Python extras remain unchanged.
Real-World Example: Order Processing
A typical business scenario: process an order with 8 typed fields, return 6 typed results.
1. The Data
JavaScript Client has order data with proper types:
import Big from 'big.js';
const orderData = {
unit_price: new Big('149.99'), // Decimal
quantity: 3,
discount: new Big('10.00'), // Decimal
order_date: new Date(2025, 0, 15), // Date
delivery_date: new Date(2025, 0, 20),
express: true,
customer_id: 12345,
notes: 'Handle with care'
};
// Expects response: { subtotal, tax, shipping, total, ship_date, arrival_date }Python Server has business logic expecting proper types:
async def process_order(unit_price, quantity, discount, order_date,
delivery_date, express, customer_id, notes):
"""Business logic - expects Decimal and date types."""
subtotal = unit_price * quantity
tax = subtotal * Decimal('0.22')
shipping = Decimal('15.00') if express else Decimal('5.00')
total = subtotal - discount + tax + shipping
ship_date = order_date + timedelta(days=1 if express else 3)
return {
'subtotal': subtotal, 'tax': tax, 'shipping': shipping,
'total': total, 'ship_date': ship_date, 'arrival_date': delivery_date
}2. ❌ WITHOUT TYTX: 20 Manual Conversions
JavaScript - must convert to/from strings:
// Send: convert types → strings
const response = await fetch('/api/process_order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
unit_price: orderData.unit_price.toString(), // Decimal → string
discount: orderData.discount.toString(), // Decimal → string
order_date: orderData.order_date.toISOString().slice(0, 10), // Date → string
delivery_date: orderData.delivery_date.toISOString().slice(0, 10),
quantity: orderData.quantity, express: orderData.express,
customer_id: orderData.customer_id, notes: orderData.notes
})
});
// Receive: convert strings → types
const json = await response.json();
const result = {
subtotal: new Big(json.subtotal), tax: new Big(json.tax),
shipping: new Big(json.shipping), total: new Big(json.total),
ship_date: new Date(json.ship_date), arrival_date: new Date(json.arrival_date)
};Python - must convert to/from strings:
@app.post("/api/process_order")
async def handle_order(request: Request):
json_data = await request.json()
# Receive: convert strings → types
unit_price = Decimal(json_data['unit_price'])
discount = Decimal(json_data['discount'])
order_date = date.fromisoformat(json_data['order_date'])
delivery_date = date.fromisoformat(json_data['delivery_date'])
result = await process_order(unit_price, json_data['quantity'], discount,
order_date, delivery_date, json_data['express'],
json_data['customer_id'], json_data['notes'])
# Send: convert types → strings
return JSONResponse({
'subtotal': str(result['subtotal']), 'tax': str(result['tax']),
'shipping': str(result['shipping']), 'total': str(result['total']),
'ship_date': result['ship_date'].isoformat(),
'arrival_date': result['arrival_date'].isoformat()
})Total: 20 manual conversions (4 JS→string + 4 string→Python + 6 Python→string + 6 string→JS).
3. ✅ WITH TYTX: Zero Conversions
import { fetchTytx } from '@genro/tytx';
const result = await fetchTytx('/api/process_order', { body: orderData });
console.log(result.total.toFixed(2)); // Big, ready to useTotal: 0 conversions. Types flow naturally.
Bonus: Switch to MessagePack in One Line
Need binary format for better performance? Just add transport: 'msgpack':
// JSON (default)
const result = await fetchTytx('/api/process_order', { body: orderData });
// MessagePack - same API, binary format
const result = await fetchTytx('/api/process_order', { body: orderData, transport: 'msgpack' });Supported Types
| Python | JavaScript | Wire Format |
|--------|------------|-------------|
| Decimal | Decimal (big.js) | "99.99::N" |
| date | Date (midnight UTC) | "2025-01-15::D" |
| datetime | Date | "2025-01-15T10:30:00.000Z::DHZ" |
| time | Date (epoch date) | "10:30:00.000::H" |
| bytes | Uint8Array | "AAEC::RAW" (base64; native bin on MessagePack) |
Native JSON types (string, number, boolean, null) pass through unchanged.
When to Use TYTX
Good fit:
- Web apps with forms containing dates/decimals
- Financial applications requiring decimal precision
- APIs that send/receive typed data frequently
- Excel-like grids with mixed types
Not needed:
- APIs that only use strings and integers
- Simple CRUD with no special types
- Already using GraphQL/Protobuf with full type support
Documentation
| I want to... | Go to... | |--------------|----------| | Try it in 5 minutes | Quick Start | | Use it over HTTP | HTTP Integration | | Understand the wire format | How It Works | | See API reference | API Reference | | Compare with alternatives | Alternatives |
License
Apache License 2.0 - Copyright 2025 Softwell S.r.l.
