Private fields
Mask sensitive values so they can be sent to the client without exposing the underlying data.
Some fields need to travel back to a client for display or editing, but their actual value should never be exposed in plain text — passwords, secret keys, API tokens, card numbers. The setPrivate(true) flag handles exactly this case.
How it works
A private field's value is wrapped in a PrivateValue container:
type PrivateValue<T> =
| { state: 'plain'; value?: T } // actual value, for internal use
| { state: 'masked'; value: string } // opaque mask, safe to send to the clientWhen the server sends a record to the client, it replaces the actual value with { state: 'masked', value: '••••••••' }. The client can display the mask, but it never sees the real value.
When the client submits the form back, it can either:
- Send the mask unchanged (meaning "don't change this field")
- Send a new
{ state: 'plain', value: '...' }object (meaning "replace with this value")
The validator understands both cases.
Defining a private field
import * as d from 'dynz';
const userSchema = d.object({
email: d.string().email(),
password: d.string().min(8).setPrivate(true),
});The inferred type SchemaValues<typeof userSchema> will be:
{
email: string;
password: PrivateValue<string>;
}Server: mask before sending
When loading a record to send to the client, replace plain values with masked ones:
import { getPrivateData, type PrivateValue } from 'dynz';
const dbUser = await db.users.findById(userId);
// Replace the actual password hash with a display mask
const clientUser = {
...dbUser,
password: { state: 'masked', value: '••••••••' } satisfies PrivateValue<string>,
};
return Response.json(clientUser);Client: handle plain and masked values
Your form renders the mask as a placeholder. When the user types a new value, wrap it as plain:
// User didn't touch the password field — send the mask back unchanged
const payload = {
email: form.email,
password: { state: 'masked', value: '••••••••' },
};
// User typed a new password — send it as plain
const payload = {
email: form.email,
password: { state: 'plain', value: form.newPassword },
};Server: validate the update
On the server, validate the incoming payload using the schema with private fields. Dynz handles both cases:
- If
password.state === 'masked'— the value hasn't changed; validation passes and the original value is preserved. - If
password.state === 'plain'— the new value is validated against all rules (.min(8)etc.) and stored.
const result = await d.validate(
userSchema,
{ email: existingUser.email, password: { state: 'plain', value: existingUser.passwordHash } },
incomingPayload,
);
if (result.success) {
// result.values.password is PrivateValue<string>
// Check state to decide whether to update the DB
if (result.values.password.state === 'plain') {
await db.users.updatePassword(userId, hash(result.values.password.value));
}
}TypeScript types
SchemaValues<T> reflects the private wrapper in the type:
const schema = d.object({
apiKey: d.string().setPrivate(true),
});
type Record = d.SchemaValues<typeof schema>;
// { apiKey: PrivateValue<string> }The PrivateValue<T> type is exported from dynz:
import type { PrivateValue } from 'dynz';