Encode & decode rows
Your table definition gives you encode and decode, the bridge between app values and wire values. This guide shows the four everyday operations against a live database with the surrealdb SDK. The rule throughout: encode to write, decode to read.
The examples assume a connected client:
import { Surreal } from "surrealdb";
import { User } from "./schema/user";
const db = new Surreal();
await db.connect("ws://localhost:8000", {
namespace: "app",
database: "app",
auth: { username: "root", password: "root" },
});Create a row
encode produces the wire payload for a full insert. Fields with a database default are optional, so you omit them. Write it with a CREATE ... CONTENT query:
const [[created]] = await db.query("CREATE user CONTENT $data", {
data: User.encode({ name: "Ada Lovelace", email: "ada@example.com" }),
});db.query returns one result set per statement, each an array of records — hence the double-destructure. User.encode(...) accepts the Create shape and returns a wire object: a Date you passed becomes a DateTime, a uuid string becomes a Uuid, and so on.
For a record whose id you choose, the typed db.create also works when you pass a RecordId (not a string):
import { RecordId } from "surrealdb";
const created = await db.create(
new RecordId("user", "ada"),
User.encode({ name: "Ada Lovelace", email: "ada@example.com" }),
); // returns the created record objectRead a row
decode validates a returned row and converts it to app values:
import type { App } from "@schemic/core";
const rows = await db.select("user");
const users: App<typeof User>[] = rows.map((row) => User.decode(row));
users[0].createdAt instanceof Date; // trueAlways decode what you read. A raw row holds wire values (DateTime, Uuid); decoding turns them into the Dates and strings your code expects, and validates the row against your schema in the process.
Update a row
For a partial update, encodePartial produces a wire payload where every field is optional — built for MERGE, UPDATE, and PATCH. Apply it with an UPDATE ... MERGE query:
await db.query("UPDATE $id MERGE $patch", {
id: new RecordId("user", "ada"),
patch: User.encodePartial({ name: "Augusta Ada King" }),
});The typed builder form works too: db.update(id).merge(User.encodePartial({ ... })).
Send only the fields that changed. encodePartial runs the same codecs as encode, so a partial { lastSeenAt: new Date() } still encodes the Date to a DateTime.
Handle invalid data safely
The throwing methods raise a ZodError on invalid input. The safe* variants return a result object instead, mirroring Zod’s safeParse:
const result = User.safeDecode(unknownRow);
if (result.success) {
use(result.data);
} else {
console.error(result.error.issues);
}safeDecode, safeEncode, and safeEncodePartial cover the three operations. For schemas with async refinements, use decodeAsync / encodeAsync / encodePartialAsync.
Convert a single value
When you have one value rather than a whole row, the field schema itself exposes .encode(value) and .decode(value):
const createdAt = s.datetime();
const wire = createdAt.encode(new Date()); // Date -> DateTime
const app = createdAt.decode(wire); // DateTime -> DateWhere to go next
- The encoded and decoded sides — the model behind these methods.
- Codecs — what each field converts, and writing custom codecs.
- Type utilities —
App,Wire,Create,Update.