Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

Omitting Fields

Hide scalar fields from results with omit — per query, per client, or once in the schema

omit is the inverse of select: it returns every readable scalar EXCEPT the ones you name.

const users = await client.user.findMany({
  omit: { passwordHash: true },
});
// Type: { id: string; email: string; name: string }[]

Use it when a model has one or two fields you want gone and a dozen you want kept — the select spelling of the same query has to list all twelve, and grows a hole every time someone adds a column.

Where it works

omit is accepted on every operation that returns a model row:

findUnique, findUniqueOrThrow, findFirst, findFirstOrThrow, findMany, create, update, upsert, delete, and the bulk writes createMany, updateMany, deleteMany.

On a bulk write, omit is a projection, so — exactly like select — its presence is what makes the operation return the affected rows instead of { count }:

const rows = await client.note.updateMany({
  where: { archived: false },
  data: { archived: true },
  omit: { draftBody: true },
});
// Type: { id: string; title: string; archived: boolean }[]

omit and select are mutually exclusive

select states the projection positively, omit states it negatively. A payload carrying both states it twice, so it is refused at the parse boundary, before any query runs:

await client.user.findMany({
  select: { id: true },
  omit: { passwordHash: true },
});
// ValidationError: Mutually exclusive fields cannot be used together: select, omit

An omit that names every readable field is refused for the same reason an empty select: {} is — there is no such result:

await client.tag.findMany({ omit: { id: true, label: true } });
// ValidationError: 'omit' on 'findMany' excluded every readable field of model 'tag'.

omit composes with include

include adds relations on top of the reduced scalar set, and each relation node takes its own omit:

const users = await client.user.findMany({
  omit: { passwordHash: true },
  include: {
    posts: {
      omit: { draftBody: true },
      orderBy: { createdAt: "desc" },
      take: 5,
    },
  },
});

The two are independent: the outer omit names fields of user, the inner one names fields of post. A nested node follows the same rules as the top level — select and omit cannot coexist on it, and it cannot empty its own projection.

The three layers, and which one wins

There are three places a field can be hidden. They are not variants of one mechanism, and the difference is the whole story:

Layer Where What it means Can a query undo it?
Model-level s.model({…}).omit({ passwordHash: true }) Schema truth: this column does not leave the database No
Client-level createClient({ omit: { user: { passwordHash: true } } }) This client’s default Yes
Query-level findMany({ omit: { passwordHash: true } }) This call

Model-level .omit() is a hard exclusion

It exists for secrets, so it is not overridable by anything downstream. The field has no select key and no omit key at all — naming it is a parse failure, not a silently empty column:

const vault = s
  .model({ id: s.string().id(), label: s.string(), secret: s.string() })
  .omit({ secret: true });

await client.vault.findMany({});
// { id: string; label: string }[] — `secret` is not in the SQL either

await client.vault.findMany({ select: { secret: true } });
// ValidationError: Unknown key: secret

await client.vault.findMany({ omit: { secret: false } });
// ValidationError: Unknown key: secret

A query-level omit still works on whatever remains:

await client.vault.findMany({ omit: { label: true } });
// { id: string }[]

.omit() is keyed to the model’s own scalars, so the names it takes are checked and autocompleted. A typo, a relation name, or a false is a compile error rather than a key that hides nothing — which is the whole point when the field is a secret:

s.model({ id: s.string().id(), secret: s.string() }).omit({ scret: true });
//                                                          ^^^^^
// Type 'true' is not assignable to type 'never' — 'scret' is not a scalar

s.model({ id: s.string().id(), posts: s.oneToMany(() => post) })
  .omit({ posts: true });
// a relation is not a projectable scalar

s.model({ id: s.string().id(), secret: s.string() }).omit({ secret: false });
// Type 'false' is not assignable to type 'true' — .omit() only ever hides

The refusal is per key, not per call. Hiding two secrets and misspelling one still fails, on the misspelled one — the case where an “unknown-properties” check that only fires when every key is wrong would let the column leak:

s.model({ id: s.string().id(), secret: s.string(), token: s.string() })
  .omit({ secret: true, tokne: true });
//                      ^^^^^ Type 'true' is not assignable to type 'never'

It is also per key regardless of how the object reaches the call. An as const, a spread, a pre-annotated variable and a widened Record<string, true> are all refused the same way — a name the model does not have never reaches the state, so the result type can never claim a column is hidden that is still being returned.

Client-level omit is a default

const client = createClient({
  schema: { user, note },
  driver,
  omit: {
    user: { passwordHash: true },
    note: { draftBody: true },
  },
});

It applies to every result of those models, including relation payloads reached through include:

await client.user.findMany({ include: { notes: true } });
// user rows without passwordHash, note rows without draftBody

A query overrides it, per field, with false:

await client.user.findMany({ omit: { passwordHash: false } });
// passwordHash is back, for this call only

…and adds to it with true:

await client.user.findMany({ omit: { email: true } });
// email AND passwordHash are gone (the client default still applies)

An explicit select overrides it wholesale — naming a field is asking for it:

await client.user.findMany({ select: { id: true, passwordHash: true } });
// { id: string; passwordHash: string }[]

The flags are true only. A false here would be a key that does nothing — the client default is the thing being set, and per-field re-inclusion belongs on the query, where it can be undone:

createClient({ schema, driver, omit: { user: { passwordHash: false } } });
// Type 'false' is not assignable to type 'true'

A client omit naming a model or a field that does not exist is rejected when the client is constructed, not silently ignored.

The client default is in the types too

A field this client hides is absent from the default result type, not merely missing at runtime:

const client = createClient({
  schema: { user, post },
  driver,
  omit: { user: { passwordHash: true } },
});

const user = await client.user.findUnique({ where: { id } });
user.passwordHash;
// Property 'passwordHash' does not exist

const post = await client.post.findMany();
// unchanged — the config named `user`, not `post`

The per-field override and the select rule are type-level too:

await client.user.findMany({ omit: { passwordHash: false } });
// { id: string; email: string; passwordHash: string }[] — restored

await client.user.findMany({ select: { id: true, passwordHash: true } });
// { id: string; passwordHash: string }[] — a select is untouched

await client.user.updateMany({ where, data });
// { count: number } — a default never flips a bulk write's return shape

Types follow the runtime

The result type drops the same keys the runtime does, so an omitted field is a compile error rather than an undefined at 3am:

const user = await client.user.findUnique({
  where: { id },
  omit: { passwordHash: true },
});
user.passwordHash;
// Property 'passwordHash' does not exist

One case cannot be decided statically: a flag whose type is a widened boolean rather than a literal.

const hide: boolean = shouldHide();
const rows = await client.user.findMany({ omit: { passwordHash: hide } });
// passwordHash?: string — present or not, only the runtime knows

The key becomes optional instead of being guessed into one arm. The same honesty applies to a bulk write whose omit may be undefined: the result type is the union of { count } and the row array, and the caller narrows.

A client-level default follows the same rule when the config is what cannot be pinned down — an omit?: that may or may not be there gives an optional key, never a present one:

type Config = { schema: typeof schema; driver: Driver; omit?: typeof defaults };
const client = createClient(config as Config);
await client.user.findMany();
// { id: string; email: string; passwordHash?: string }[]

What the types can and cannot see

One gap, on the record rather than papered over: the client default reduces the node the operation is called on, not relation payloads reached through include.

const client = createClient({ schema, driver, omit: { note: { draft: true } } });

await client.note.findMany();
// { id: string; body: string }[] — draft is gone from the type

await client.user.findMany({ include: { notes: true } });
// notes still typed with `draft` — the runtime drops it anyway

The runtime does apply the default there (see the example under “Client-level omit is a default” above), so the value is genuinely absent; only the type is wider than the row. The reason is structural: a relation resolves to a target model, and the config is written against schema keys, so recovering the key would mean comparing model types — the comparison that collapses mutually-recursive schemas. Name the field in a nested omit if you want the type to agree:

await client.user.findMany({ include: { notes: { omit: { draft: true } } } });
// notes: { id: string; body: string }[]

Prisma compatibility

Query-level and client-level omit match Prisma’s semantics, including the select/omit exclusivity, the local-overrides-global rule, and the refusal of an omit that empties the result.

Two deliberate differences:

  • Model-level .omit() has no Prisma counterpart. Prisma has only the client-level default, which a select can always undo. viborm’s schema-level exclusion cannot be undone, which is what makes it usable for secrets.
  • omit on a bulk write returns rows. Prisma has no returning deleteMany at all, and reaches the returning form of the others through the separate createManyAndReturn / updateManyAndReturn methods, which viborm does not have (see compatibility).

Was this page helpful?