Dynz.

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 client

When 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';

On this page