From a5a7a7b741ad0de92cb604eec8c9da0197a897bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 10:00:58 +0000 Subject: [PATCH] =?UTF-8?q?fix(docs):=20the=20schema-extension=20FAQ=20rot?= =?UTF-8?q?ted=20in=20reverse=20=E2=80=94=20pin=20the=20claim=20in=20the?= =?UTF-8?q?=20checked=20block=20this=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3890 taught that FieldSchema/ObjectSchema/ActionSchema are ZodPipes and that FieldSchema.extend throws. True when written; protocol 17 (#3855, acbf364a0) then retired the aliases whose lowering was the whole reason for the field/object transforms, the pipes collapsed to plain ZodObjects, 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 (requiresFeature -> visible lowering, still live per the ledger) remains a pipe. All verified against a clean build: FieldSchema bound ZodObject .extend: function .in: undefined ObjectSchema bound ZodObject .extend: function .in: undefined ActionSchema bound ZodPipe .extend: undefined .in: ZodObject The example gate never noticed because #3890's checked block used only .parse() — deliberately shape-agnostic after CI rejected the .in.extend attempt. That made the code durable and left the PROSE as the only load-bearing surface, which is exactly where the rot settled. The rewrite moves the claim into the checked block: it calls FieldSchema.extend({ ... }) directly, so a future shape flip fails CI loudly instead of the prose going quietly wrong. Composition stays as the shape-agnostic default; ActionSchema is the documented pipe case; 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. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Ajwvrmd1hDC9RBofYBhGuR --- .changeset/docs-extend-faq-reversed-rot.md | 27 ++++++++++++++ content/docs/deployment/troubleshooting.mdx | 40 +++++++++++++-------- 2 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 .changeset/docs-extend-faq-reversed-rot.md 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?