Validation and codecs
Validate writes and transform application values to Couchbase JSON.
CouchSet keeps application types and JSON-on-the-wire separate. Dates and custom values use codecs (or simple date schema helpers) at the SDK boundary. Create/replace hooks validate full documents after CouchSet adds persistence metadata. Patch and raw mutation paths intentionally skip full-document validation.
Dates without a custom codec
Two equivalent ways to parse stored values into Date on read:
import { defineModel } from 'couchset/next';
// Top-level schema date
defineModel({
name: 'Event',
schema: { happenedAt: 'date' },
});
// Nested paths via dateFields (also merged into schema as date)
defineModel({
name: 'Event',
dateFields: ['metadata.receivedAt', 'profile.birthDate'],
});Generated createdAt / updatedAt metadata is handled automatically. dateFields support dotted paths; missing intermediate objects are left alone.
Field codecs
A FieldCodec is { toDatabase, fromDatabase }. Paths may be top-level keys or dotted strings (payload.expiresAt).
import { dateCodec, defineModel } from 'couchset/next';
type Money = { amount: number; currency: string };
const invoices = defineModel<{
issuedAt: Date;
total: Money;
payload?: { expiresAt: Date };
}>({
name: 'Invoice',
codecs: {
issuedAt: dateCodec,
total: {
toDatabase: (v: Money) => ({ amount: v.amount, currency: v.currency }),
fromDatabase: (v) => v as Money,
},
'payload.expiresAt': dateCodec,
},
});dateCodec ships from couchset/next:
// toDatabase: Date -> ISO string
// fromDatabase: string -> DateWhen codecs run
| Direction | Operations |
|---|---|
toDatabase | insert, upsert, replace (full serialize); matching $set fields via serializeField |
fromDatabase | reads through parse / deserialize |
Rules:
- A field cannot also use
schema: { field: 'date' }or appear indateFields— CouchSet throws at model construction (codec for … conflicts with dateFields or schema date). - Undefined values are skipped (no codec call).
- Partial projections decode only present fields; full-document
parsehooks are not run on projections (parseProjection). - Included related models use their own codecs.
- Raw/computed SQL++ projections must not assume document codecs.
Validation hooks
validateCreate runs on insert/upsert after metadata (id, createdAt, updatedAt, _type, _scope) is attached. validateReplace runs on replace. Each hook may be async and may return a transformed document (or void to keep the input).
const users = defineModel<User>({
name: 'User',
validateCreate: async (document) => {
if (!document.email?.includes('@')) throw new Error('invalid email');
return document;
},
validateReplace: (document) => document,
});prepareInsert / prepareReplace use the same validation + serialize pipeline as the model write methods.
What is not validated
patchById/ subdocument$set/$unset/$incmutateById,incrementById,consumeOnce- Manual
parse/ hydrate paths that are output transforms only
Treat those as escape hatches: validate in application code if you need checks there.
Standard Schema via withModelValidator
withModelValidator(schema) is a plugin that adapts Standard Schema v1 into both validateCreate and validateReplace.
import { defineModel, withModelValidator, ModelValidationError } from 'couchset/next';
const users = defineModel<User>({
name: 'User',
plugins: [withModelValidator(userStandardSchema)],
});
try {
await db.model(users).insert({ email: 'bad' });
} catch (error) {
if (error instanceof ModelValidationError) {
// error.issues: { message, path? }[]
}
}Behavior:
- Requires
schema['~standard'].version === 1and avalidatefunction. - Supports sync or async validate; failures throw
ModelValidationErrorwith structuredissues. - If the schema library strips unknown keys, CouchSet re-applies persistence metadata from the pre-validation document:
id,_type,_scope,createdAt,updatedAt. - Plugin composition conflicts if another contribution already set
validateCreate/validateReplace(validators are not silently replaced). - Inside transactions, validation hooks may run more than once — keep them deterministic and side-effect free.
parse vs validation
| API | Role |
|---|---|
validateCreate / validateReplace | Gate / transform documents before they are stored |
parse | Output transform after deserialize + schema/date/codecs on read |
parseProjection | Decode present fields without full-document parse hooks |
Do not use parse as a substitute for write validation.
Pitfalls
- Mixing
dateCodec(or any codec) withdateFields/schema: 'date'on the same path fails fast at definition time. - Expecting patch paths to run
validateCreate— they do not. - Assuming a Standard Schema strip of unknown fields removes
id/_type— CouchSet puts those back after validate. - Stacking two validator plugins that both set create/replace hooks — second contribution conflicts.
- Using codecs on include roots incorrectly — each model owns its codec map.
See also
- Models and definitions —
defineModel, registries, metadata fields - CRUD and queries — insert, replace, patch behavior
- Plugins, hooks, scopes, and operations — plugin composition
- API reference —
dateCodec,withModelValidator,ModelValidationError