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 aselectcan always undo. viborm’s schema-level exclusion cannot be undone, which is what makes it usable for secrets. omiton a bulk write returns rows. Prisma has no returningdeleteManyat all, and reaches the returning form of the others through the separatecreateManyAndReturn/updateManyAndReturnmethods, which viborm does not have (see compatibility).