diff --git a/.changeset/docs-extend-faq-reversed-rot.md b/.changeset/docs-extend-faq-reversed-rot.md new file mode 100644 index 0000000000..69dcf0328a --- /dev/null +++ b/.changeset/docs-extend-faq-reversed-rot.md @@ -0,0 +1,27 @@ +--- +"@objectstack/spec": patch +--- + +fix(docs): the schema-extension FAQ rotted in reverse — `.extend()` works on `FieldSchema` again since protocol 17 + +The #3890 fix taught that `FieldSchema` / `ObjectSchema` / `ActionSchema` are all +`ZodPipe`s and that `FieldSchema.extend` throws. True when written; protocol 17 +(#3855) then retired the deprecated aliases whose lowering was the whole reason +for the field/object transforms, the pipes collapsed to plain `ZodObject`s, and +both prose claims inverted within days: `.extend` works, and the recommended +`FieldSchema.in` is now `undefined` — following the FAQ was once again the only +way to hit an error. Only `ActionSchema` (whose `requiresFeature` → `visible` +lowering is still live) remains a pipe. + +The example gate never noticed because the checked block used only `.parse()` — +deliberately shape-agnostic after CI rejected the first #3890 attempt. That made +the code durable and left the PROSE as the only load-bearing surface, which is +where the rot settled. + +So the rewrite moves the claim into the checked block: it now calls +`FieldSchema.extend({ … })` directly, so if the schema ever grows a transform +again the gate goes red instead of the prose going quietly wrong. Composition +stays as the shape-agnostic default, `ActionSchema` is documented as the pipe +case with the `.in.extend` caveat, and the FAQ teaches the one-line probe +(`typeof SomeSchema.extend === 'function'`) instead of a table of shapes that +history says will not stay true. diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx index c73ada4e30..0ee52a786f 100644 --- a/content/docs/deployment/troubleshooting.mdx +++ b/content/docs/deployment/troubleshooting.mdx @@ -364,34 +364,44 @@ Yes. All schemas work with plain JavaScript. You lose compile-time type checking ### How do I extend a built-in schema? -**Not with `.extend()` on the schema itself.** Several built-in schemas — -`FieldSchema`, `ObjectSchema`, `ActionSchema` — carry a `.transform()` that lowers -author-facing sugar at parse time, which makes the exported value a **`ZodPipe`, -not a `ZodObject`**. `FieldSchema.extend` is `undefined`, so calling it throws -`is not a function`. - -Compose instead: parse with the built-in schema, and validate your additions -alongside it. This keeps the transform running, which `.extend()` would discard. +It depends on whether the schema you are extending carries a parse-time +`.transform()`. A plain schema (`FieldSchema`, `ObjectSchema` since protocol 17 +retired their alias-lowering transforms, #3855) is a `ZodObject` and takes +`.extend()` directly. The composition form works in **either** case, and is the +safe default when you don't want to track which is which: {/* os:check */} ```typescript import { z } from 'zod'; import { FieldSchema } from '@objectstack/spec/data'; -const CustomProps = z.object({ customProperty: z.string().optional() }); +// Plain object schema: .extend() works directly. (This line is what pins the +// claim — if FieldSchema ever grows a transform again and becomes a ZodPipe, +// .extend vanishes and this example fails CI instead of the prose going stale.) +const CustomFieldSchema = FieldSchema.extend({ + customProperty: z.string().optional(), +}); +CustomFieldSchema.parse({ name: 'code', type: 'text', customProperty: 'x' }); +// Composition: parse with the built-in schema, validate additions alongside. +// Shape-agnostic — works for plain schemas AND transform-carrying pipes. +const CustomProps = z.object({ customProperty: z.string().optional() }); function parseCustomField(input: unknown) { return { ...FieldSchema.parse(input), ...CustomProps.parse(input) }; } - parseCustomField({ name: 'code', type: 'text', customProperty: 'x' }); ``` -If you genuinely need one merged schema object, `FieldSchema.in` is the -`ZodObject` the pipe wraps, so `FieldSchema.in.extend({ … })` builds one — but -the result **skips the transform**, so author-facing sugar the pipe would have -lowered stays raw. Prefer the composition above unless you specifically want the -untransformed shape. +A schema that still lowers author-facing sugar at parse time — `ActionSchema` +(its `requiresFeature` → `visible` lowering) is one, as of protocol 17 — is a +**`ZodPipe`**, where `.extend` does not exist: prefer the composition form. +Reaching the inner object via `.in.extend({ … })` builds a merged schema but +**skips the transform**, so the sugar the pipe would have lowered stays raw. + +A quick check when unsure: `typeof SomeSchema.extend === 'function'` — a pipe +reports `undefined`. This page once asserted the shapes the other way around; +the schemas moved under it within days (#3890 → #3855), which is why the +load-bearing claim above now lives in a CI-checked block rather than prose. ### Where are the JSON Schemas for IDE autocomplete?