# One field, several targets (/docs/schema/relations/polymorphic)

---
title: One field, several targets
description: Pass a map of collections instead of a name and the field stores which collection it points at alongside the id.
kind: guide
package: questpie
---

A comment can hang off a blog post or off a page. One `f.relation("posts")`
cannot say that, so the field takes a map of the collections it is allowed to
point at.

## Declaring it

```ts title="src/questpie/server/collections/comments.ts"
import { collection } from "#questpie/factories";

export const comments = collection("comments").fields(({ f }) => ({
	body: f.textarea().required(),
	subject: f.relation({ posts: "posts", pages: "pages" }).required(),
}));
```

Each key is the value stored in the discriminator, and each value names the
collection it stands for. The keys are checked against the same registry as the
values, so a discriminator has to be a collection name. The field becomes two
columns: `subjectType`, a `varchar` sized to the longest key with a floor of 50
characters, and `subjectId`, a `varchar(36)`.

## Reading and writing it

The two columns are an implementation detail. Both the read shape and the write
shape are a single object.

```ts
await client.collections.comments.create({
	body: "Booked, thanks.",
	subject: { type: "posts", id: postId },
});

const comment = await client.collections.comments.findOne({ where: { id } });
// comment.subject → { type: "posts", id: "0d9a…" }
```

Writing a `type` that is not one of the map's keys is a `400 Bad Request`, as is
anything that is not a `{ type, id }` pair. When both columns are null the field
reads back as `null`. `.required()` is enforced when the payload is validated,
and the two columns themselves stay nullable.

Fetching the row it names is a second call against whichever collection `type`
identifies. A polymorphic field has no single target, so `with` cannot resolve
it.

```ts
const subject =
	comment.subject?.type === "posts"
		? await client.collections.posts.findOne({
				where: { id: comment.subject.id },
			})
		: await client.collections.pages.findOne({
				where: { id: comment.subject.id },
			});
```

## When to reach for it

Use the map when a field genuinely points at more than one collection and the
set is open enough that a column per target would be worse. Two nullable
belongsTo fields are easier to query and easier to constrain, so prefer them
whenever the list of targets is short and stable.

## Related

- **[Relations](/docs/schema/relations)** for the single-target form and its
  query surface.
- **[Blocks](/docs/schema/blocks)** for content whose shape varies by entry,
  which is the other problem this looks like.
