{"record":{"id":"d01bef4774abf678","repo":"withastro/astro","slug":"contentloaderreturnsinvalidid","errorCode":"ContentLoaderReturnsInvalidId","errorMessage":"The content loader for the collection **${String(collection)}** returned an entry with an invalid `id`:\n${JSON.stringify(entry, null, 2)}","messagePattern":"The content loader for the collection \\*\\*(.+?)\\*\\* returned an entry with an invalid `id`:\n(.+?)","errorType":"error_code","errorClass":"AstroError","httpStatus":null,"severity":"error","filePath":"packages/astro/src/content/content-layer.ts","lineNumber":492,"sourceCode":") {\n\tconst unsafeData = await handler();\n\tconst parsedData = loaderReturnSchema.safeParse(unsafeData);\n\n\tif (!parsedData.success) {\n\t\tconst issue = parsedData.error.issues[0] as z.core.$ZodIssueInvalidUnion;\n\n\t\t// Due to this being a union, zod will always throw an \"Expected array, received object\" error along with the other errors.\n\t\t// This error is in the second position if the data is an array, and in the first position if the data is an object.\n\t\tconst parseIssue = Array.isArray(unsafeData) ? issue.errors[0] : issue.errors[1];\n\n\t\tconst error = parseIssue[0];\n\t\tconst firstPathItem = error.path[0];\n\n\t\tconst entry = Array.isArray(unsafeData)\n\t\t\t? unsafeData[firstPathItem as number]\n\t\t\t: unsafeData[firstPathItem as string];\n\n\t\tthrow new AstroError({\n\t\t\t...AstroErrorData.ContentLoaderReturnsInvalidId,\n\t\t\tmessage: AstroErrorData.ContentLoaderReturnsInvalidId.message(context.collection, entry),\n\t\t});\n\t}\n\n\tconst data = parsedData.data;\n\n\tcontext.store.clear();\n\n\tif (Array.isArray(data)) {\n\t\tfor (const raw of data) {\n\t\t\tif (!raw.id) {\n\t\t\t\tthrow new AstroError({\n\t\t\t\t\t...AstroErrorData.ContentLoaderInvalidDataError,\n\t\t\t\t\tmessage: AstroErrorData.ContentLoaderInvalidDataError.message(\n\t\t\t\t\t\tcontext.collection,\n\t\t\t\t\t\t`Entry missing ID:\\n${JSON.stringify({ ...raw, id: undefined }, null, 2)}`,\n\t\t\t\t\t),","sourceCodeStart":474,"sourceCodeEnd":510,"githubUrl":"https://github.com/withastro/astro/blob/52e6c34790cc8ac4e69e6135ace06049867e5c4a/packages/astro/src/content/content-layer.ts#L474-L510","documentation":"Astro content collections accept two loader styles: a full Loader object, or a plain async function (CollectionLoader). When you use the function style, its return value is validated against loaderReturnSchema: either an array of objects where each has a string `id`, or a record of string keys to objects with an optional string `id`. If validation fails, Astro throws ContentLoaderReturnsInvalidId and JSON-stringifies the offending entry so you can see exactly which one is malformed.","triggerScenarios":"A function-style loader (e.g. `loader: async () => (await fetch(url)).json()`) returning entries whose `id` is a number (common with APIs/YAML/CSV), null, or missing entirely in the array form; or returning something that is neither an array nor a plain object (e.g. a string, a Map).","commonSituations":"Fetching CMS/API data where ids are numeric; parsing YAML or CSV sources that deserialize ids as non-strings; copying a `store.set`-based loader example but returning from the function instead; untyped loaders so TypeScript never flags the shape.","solutions":["Make every entry's `id` a string: map the data before returning it, e.g. `posts.map((p) => ({ ...p, id: String(p.id) }))`.","Return the correct shape: an `Array<{ id: string, ... }>` or a `Record<string, { id?: string, ... }>` — nothing else.","Add explicit TypeScript typing or a zod schema inside your loader so bad ids are caught at the boundary with a clearer message.","Read the JSON-stringified entry in the error message to find which item failed, then fix that item at the source."],"exampleFix":"// before\nconst Blog = defineCollection({\n  loader: async () => {\n    const res = await fetch('https://api.example.com/posts');\n    return res.json(); // [{ id: 1, title: '...' }] -> id is a number\n  },\n});\n\n// after\nconst Blog = defineCollection({\n  loader: async () => {\n    const res = await fetch('https://api.example.com/posts');\n    const posts: Array<Record<string, any>> = await res.json();\n    return posts.map((p) => ({ ...p, id: String(p.id) }));\n  },\n});","handlingStrategy":"validation","validationCode":"function assertLoaderReturnShape(data: unknown): void {\n  if (Array.isArray(data)) {\n    data.forEach((entry, i) => {\n      if (typeof (entry as any)?.id !== 'string')\n        throw new Error(`Entry ${i} is missing a string id: ${JSON.stringify(entry)}`);\n    });\n  } else if (data && typeof data === 'object') {\n    for (const [key, value] of Object.entries(data as object)) {\n      const id = (value as any)?.id;\n      if (id !== undefined && id !== key)\n        throw new Error(`Key ${key} does not match id ${id}`);\n    }\n  } else {\n    throw new Error('Loader must return an array or a plain object');\n  }\n}\n\n// in the loader, before returning:\nconst data = await fetchData();\nassertLoaderReturnShape(data);\nreturn data;","typeGuard":"function isLoaderEntryArray(v: unknown): v is Array<{ id: string } & Record<string, unknown>> {\n  return (\n    Array.isArray(v) &&\n    v.every((e) => e !== null && typeof e === 'object' && typeof (e as any).id === 'string')\n  );\n}","tryCatchPattern":"try {\n  const posts = await getCollection('blog');\n} catch (err) {\n  if ((err as any)?.code === 'ContentLoaderReturnsInvalidId') {\n    // message contains the JSON of the offending entry: log it, fix the source, re-sync\n    console.error((err as any).message);\n  } else throw err;\n}","preventionTips":["Coerce ids at the boundary: always `String(item.id)` before returning from a function-style loader.","Type the loader: `loader: async (): Promise<Array<{ id: string }>> => ...` so the compiler enforces the shape.","Never return Maps, Sets, or class instances from a loader — plain arrays/objects only."],"tags":["content-collections","content-layer","loader","zod","entry-id"],"backgroundTag":"content-loader-schema-validation","analyzedSha":"52e6c34790cc8ac4e69e6135ace06049867e5c4a","analyzedAt":"2026-08-18T18:48:03.901Z","contentChangedAt":"2026-08-18T18:48:03.901Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}