Digging Into Prisma’s Underrated Features
Exploring Prisma Beyond Relations - Working With JSON, Filters, and Nested Writes
Published On 2025-10-08

By Swasti Jain
1. Fetching Nested Relations
Imagine you have a User table related to a Post table, which in turn is related to a Comment table. You want to query the User table to retrieve specific users by their IDs, while also including each user’s associated posts — and for each post, the related comments as well.
In Prisma, you can do that with a single query efficiently using the include flag.
1 await prisma.user.findMany({
2 where: {
3 id: userId
4 }
5 include: {
6 post: {
7 include: {
8 comment: true
9 }
10 },
11 },
12 });To extract only certain fields, you can combine include with select.
1 const listings = await prisma.listing.findMany({
2 where,
3 orderBy,
4 include: {
5 author: {
6 select: { name: true },
7 },
8 location: {
9 select: {
10 timestamp: true
11 },
12 include: {
13 coordinates: true,
14 },
15 },
16 },
17 });2. Adding relations
You must be familiar with adding relations using the foreign key like this:
1await prisma.post.create({
2 data: {
3 title: "My new post",
4 userId: userId, // directly setting FK column
5 },
6})There is another (and better) way to do it:
Using connect
1await prisma.post.update({
2 where: { id: postId },
3 data: {
4 user: {
5 connect: { id: newUserId },
6 },
7 },
8});What this does is create a Foreign Key under the hood. It also validates if the user exists, providing additional safety, unlike the first approach. And for the same reason, it is comparatively slower. You don’t need to know about the FK, just the relation. This is useful when:
• You’re doing multi-level nested writes.
• You care about the relation validation.
3. Uncommon operators for querying relations
For relations, there are special operators provided by Prisma. For example, we have this schema:
1model User {
2 id String @id @default(cuid())
3 name String
4 posts Post[] // 1 user → many posts
5}
6
7model Post {
8 id String @id @default(cuid())
9 title String
10 published Boolean
11 userId String
12 user User @relation(fields: [userId], references: [id])
13}a. Some — “at least one related record matches”
Returns users who have at least one post matching the condition.
1const users = await prisma.user.findMany({
2 where: {
3 posts: {
4 some: {
5 published: true,
6 },
7 },
8 },
9});b. every — “all related records match”
Returns users for whom every post matches the condition.
1const users = await prisma.user.findMany({
2 where: {
3 posts: {
4 every: {
5 published: true,
6 },
7 },
8 },
9});c. none — “no related record matches”
1const users = await prisma.user.findMany({
2 where: {
3 posts: {
4 none: {
5 published: true,
6 },
7 },
8 },
9});In summary:

4. Querying for primitive and non-primitive types
Prisma handles querying differently for primitive and non-primitive types. Here is the difference:
a. For primitive types
Primitive fields are directly stored as scalar columns on the table. You can filter them using simple comparison operators (and these are fully type-safe).
1await prisma.user.findMany({
2 where: {
3 age: { gt: 18, lt: 50 }, // numeric comparison
4 email: { contains: "@gmail.com" }, // text comparison
5 isActive: true, // direct boolean
6 createdAt: { gte: new Date("2024-01-01") },
7 },
8});So:
- Direct mapping to DB operators (=, <, >, ILIKE)
- Index-friendly, because they correspond to scalar columns
- Very fast and fully composable (AND, OR, NOT, etc.)
b. For non-primitive types
1. Relations
When filtering by a related table, Prisma gives you relation filters (some, every, none):
1// Users with at least one published post
2await prisma.user.findMany({
3 where: {
4 posts: {
5 some: { published: true },
6 },
7 },
8});
9
10// Users with no unpublished posts
11await prisma.user.findMany({
12 where: {
13 posts: {
14 every: { published: true },
15 },
16 },
17});2. JSON
JSON data is stored as a blob (jsonb in Postgres). You can only use a limited set of filters because Prisma doesn’t expose arbitrary JSON path operators.
1// Match entire JSON object
2where: { skin: { equals: { name: "Red Luger" } } }
3
4// Check if array contains a value
5where: { attributes: { has: { rarity: "Godly" } } }3. compound/list fields
If it’s an array of scalars (String[], Int[]), Prisma gives special operators like has, hasSome, isEmpty.
1// posts with at least one of these tags
2where: { tags: { hasSome: ["tech", "js"] } }
3
4// posts where tags array contains exactly one tag
5where: { tags: { has: "flutter" } }
6
7// posts with no tags
8where: { tags: { isEmpty: true } }5. Array of JSON vs Array of relations (many-to-many relation)
In Prisma, if you have a schema that has a field of type - array of JSON, Prisma gives you JsonNullableListFilter in the where command.
So, for models like:
1model Product {
2 id String @id @default(cuid())
3 specs Json[] // ← array of JSON values in one column
4}You actually use JsonNullableListFilter, which has operators like equals, has, hasSome, hasEvery, hasNone.
1const products = await prisma.product.findMany({
2 where: {
3 specs: {
4 isEmpty: false,
5 equals: [ { key: "color", value: "red" } ],
6 },
7 },
8});If you have one-to-many or many-to-many relations, you use RelationFilters. Consider this schema:
1model User {
2 id String @id @default(cuid())
3 posts Post[]
4}
5
6model Post {
7 id String @id @default(cuid())
8 title String
9 userId String
10 user User @relation(fields: [userId], references: [id])
11}Then posts is a relation field, and Prisma lets you use operators like some, every, none:
1posts: { some: { title: { contains: 'Prisma' } } } //post with atleast one title containing "Prisma"Prisma doesn’t support partial or field-path filters on JSON values yet. So if you try to do something like the following, it won’t work.
1where: {
2 author: {
3 has: { name: { contains: "Jake" } }, // ❌ won't work
4 },
5},6. Referential Actions
Referential actions are policies that define how a referenced record is handled by the database when you run an update or delete query.
Prisma ORM supports the following referential actions:
for a schema like this:
1model User {
2 id Int @id @default(autoincrement())
3 posts Post[]
4}
5
6model Post {
7 id Int @id @default(autoincrement())
8 title String
9 tags TagOnPosts[]
10 User User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
11 userId Int?
12}
13
14model TagOnPosts {
15 id Int @id @default(autoincrement())
16 post Post? @relation(fields: [postId], references: [id], onUpdate: Cascade, onDelete: Cascade)
17 tag Tag? @relation(fields: [tagId], references: [id], onUpdate: Cascade, onDelete: Cascade)
18 postId Int?
19 tagId Int?
20}
21
22model Tag {
23 id Int @id @default(autoincrement())
24 name String @unique
25 posts TagOnPosts[]
26}This model explicitly defines the following referential actions:
- If you delete a
Tag, the corresponding tag assignment is also deleted inTagOnPosts, using theCascadereferential action - If you delete a
User, the author is removed from all posts by setting the field value toNull, because of theSetNullreferential action. To allow this,UseranduserIdmust be optional fields inPost.
Sources and useful links:
- https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions#what-are-referential-actions
- https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/working-with-composite-ids-and-constraints#deleting-records-by-a-compound-id-or-unique-constraint
- https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries#filter-a-list-of-relations