withastro/astro · error · AstroError

ContentLoaderReturnsInvalidId

ContentLoaderReturnsInvalidId

Error message

The content loader for the collection **${collection}** returned an entry with an invalid `id`:
${entry}

What it means

`simpleLoader` (used when a collection loader is a function) validates the returned data against a schema that requires each entry to have a string `id`. If validation fails, Astro extracts the offending entry from the union parse issue and throws `ContentLoaderReturnsInvalidId` showing that entry, so the loader author can see exactly which record is malformed.

Source

Thrown at packages/astro/src/content/content-layer.ts:492

) {
	const unsafeData = await handler();
	const parsedData = loaderReturnSchema.safeParse(unsafeData);

	if (!parsedData.success) {
		const issue = parsedData.error.issues[0] as z.core.$ZodIssueInvalidUnion;

		// Due to this being a union, zod will always throw an "Expected array, received object" error along with the other errors.
		// This error is in the second position if the data is an array, and in the first position if the data is an object.
		const parseIssue = Array.isArray(unsafeData) ? issue.errors[0] : issue.errors[1];

		const error = parseIssue[0];
		const firstPathItem = error.path[0];

		const entry = Array.isArray(unsafeData)
			? unsafeData[firstPathItem as number]
			: unsafeData[firstPathItem as string];

		throw new AstroError({
			...AstroErrorData.ContentLoaderReturnsInvalidId,
			message: AstroErrorData.ContentLoaderReturnsInvalidId.message(context.collection, entry),
		});
	}

	const data = parsedData.data;

	context.store.clear();

	if (Array.isArray(data)) {
		for (const raw of data) {
			if (!raw.id) {
				throw new AstroError({
					...AstroErrorData.ContentLoaderInvalidDataError,
					message: AstroErrorData.ContentLoaderInvalidDataError.message(
						context.collection,
						`Entry missing ID:\n${JSON.stringify({ ...raw, id: undefined }, null, 2)}`,
					),

View on GitHub (pinned to d081033d5f)

Solutions

  1. Map each returned item to an object with a string `id` before returning from the loader.
  2. If returning an object map, use the key as the id (do not also set a conflicting `id`).
  3. Log the loader's raw return value once to inspect the shape against the schema.

Example fix

// before
const loader = () => fetch('/api/posts').then(r => r.json());
// after
const loader = () => fetch('/api/posts').then(r => r.json()).then(arr => arr.map(p => ({ id: String(p.slug), ...p })));
Defensive patterns

Strategy: validation

Validate before calling

const out = await loader();
const entries = Array.isArray(out) ? out : Object.values(out);
if (!entries.every(e => e && typeof e.id === 'string')) {
  throw new Error('Every entry must have a string id');
}

Type guard

function entriesHaveStringId(data): data is Array<{ id: string }> {
  return Array.isArray(data) && data.every(e => e && typeof e.id === 'string');
}

Prevention

When it happens

Trigger: A function-style collection loader returns an array or object whose entries fail the `{ id: string }` shape — e.g. entries where `id` is a number, missing, null, or an object.

Common situations: Fetching from a CMS whose items use `slug`/`_id` instead of `id`; transforming API responses without normalizing the id field; returning raw DB rows where the PK is named differently.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/d01bef4774abf678. Report an issue: GitHub.