Serialization (row ↔ JSON)¶
Problem: a database row has native types — Date, bigint, Uint8Array — that
don't survive JSON.stringify on their own (bigint throws, Date becomes an
uncontrolled string, blob turns into garbage). And when a JSON arrives from outside
(an HTTP request, a queue), you want to validate and coerce it back into the right
types before writing.
Solution: tempest-db-js ships a pair of functions that know the model's schema —
toJSON/stringify to go out, fromDict/parse to come in — à la Pydantic's
model_dump/model_validate.
The theory in one sentence¶
Serialization is model-driven: each function looks at the class's columns and coerces each field by its SQL type. Unknown columns are ignored; required ones that are missing become a validation error.
Output: row → JSON¶
import { Model, column, toJSON, stringify, toDict } from "tempest-db-js";
class Event extends Model {
static tablename = "events";
id = column.bigInteger().primaryKey(); // bigint
name = column.text().notNull();
at = column.datetime().notNull(); // Date
payload = column.blob().notNull(); // Uint8Array
}
const row = {
id: 9007199254740993n,
name: "deploy",
at: new Date("2026-06-29T12:00:00Z"),
payload: new Uint8Array([1, 2, 3]),
};
toJSON(Event, row);
// {
// id: "9007199254740993", // bigint → string (without losing precision)
// name: "deploy",
// at: "2026-06-29T12:00:00.000Z", // Date → ISO
// payload: "AQID", // Uint8Array → base64
// }
stringify(Event, row); // == JSON.stringify(toJSON(Event, row))
toDict vs toJSON
toDict(Model, row)returns the native values, restricted to the known columns (no coercion) — handy for passing along inside TS.toJSON(Model, row)returns a JSON-safe version (Date→ISO,bigint→string,Uint8Array→base64) — ready forJSON.stringify/ an HTTP response.
Input: JSON/dict → validated row¶
fromDict coerces each field back into its native type (string→Date/bigint/Uint8Array,
JSON.parse on json columns) and validates required ones:
import { fromDict, parse, ValidationError } from "tempest-db-js";
const row = fromDict(Event, {
id: "9007199254740993", // string → bigint
name: "deploy",
at: "2026-06-29T12:00:00.000Z", // string → Date
payload: "AQID", // base64 → Uint8Array
});
row.at instanceof Date; // true
// parse = fromDict(Model, JSON.parse(json))
const fromHttp = parse(Event, requestBodyString);
Missing a required column (notNull and no default)? ValidationError:
try {
fromDict(Event, { id: "1" }); // name, at, payload are missing
} catch (err) {
if (err instanceof ValidationError) {
console.error(err.message); // describes the missing required columns
}
}
Where this fits in an API
On an HTTP route: parse(Model, await req.text()) to validate the body on the way
in, and toJSON(Model, row) to build the response on the way out. The
BaseRepository already returns native rows — serialize only at the boundary. See the
REST API example.
Recap¶
toJSON/stringify→ row → JSON-safe (Date→ISO,bigint→string,blob→base64).toDict→ native values restricted to the columns (no coercion).fromDict/parse→ JSON/dict → validated and coerced row; throwsValidationError.- Everything driven by the model's schema — a single source of truth, just like Pydantic.