Skip to content

Commit a5a7a7b

Browse files
committed
fix(docs): the schema-extension FAQ rotted in reverse — pin the claim in the checked block this time
#3890 taught that FieldSchema/ObjectSchema/ActionSchema are ZodPipes and that FieldSchema.extend throws. True when written; protocol 17 (#3855, acbf364) 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ajwvrmd1hDC9RBofYBhGuR
1 parent ff17642 commit a5a7a7b

2 files changed

Lines changed: 52 additions & 15 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(docs): the schema-extension FAQ rotted in reverse — `.extend()` works on `FieldSchema` again since protocol 17
6+
7+
The #3890 fix taught that `FieldSchema` / `ObjectSchema` / `ActionSchema` are all
8+
`ZodPipe`s and that `FieldSchema.extend` throws. True when written; protocol 17
9+
(#3855) then retired the deprecated aliases whose lowering was the whole reason
10+
for the field/object transforms, the pipes collapsed to plain `ZodObject`s, and
11+
both prose claims inverted within days: `.extend` works, and the recommended
12+
`FieldSchema.in` is now `undefined` — following the FAQ was once again the only
13+
way to hit an error. Only `ActionSchema` (whose `requiresFeature``visible`
14+
lowering is still live) remains a pipe.
15+
16+
The example gate never noticed because the checked block used only `.parse()`
17+
deliberately shape-agnostic after CI rejected the first #3890 attempt. That made
18+
the code durable and left the PROSE as the only load-bearing surface, which is
19+
where the rot settled.
20+
21+
So the rewrite moves the claim into the checked block: it now calls
22+
`FieldSchema.extend({ … })` directly, so if the schema ever grows a transform
23+
again the gate goes red instead of the prose going quietly wrong. Composition
24+
stays as the shape-agnostic default, `ActionSchema` is documented as the pipe
25+
case with the `.in.extend` caveat, and the FAQ teaches the one-line probe
26+
(`typeof SomeSchema.extend === 'function'`) instead of a table of shapes that
27+
history says will not stay true.

content/docs/deployment/troubleshooting.mdx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -364,34 +364,44 @@ Yes. All schemas work with plain JavaScript. You lose compile-time type checking
364364

365365
### How do I extend a built-in schema?
366366

367-
**Not with `.extend()` on the schema itself.** Several built-in schemas —
368-
`FieldSchema`, `ObjectSchema`, `ActionSchema` — carry a `.transform()` that lowers
369-
author-facing sugar at parse time, which makes the exported value a **`ZodPipe`,
370-
not a `ZodObject`**. `FieldSchema.extend` is `undefined`, so calling it throws
371-
`is not a function`.
372-
373-
Compose instead: parse with the built-in schema, and validate your additions
374-
alongside it. This keeps the transform running, which `.extend()` would discard.
367+
It depends on whether the schema you are extending carries a parse-time
368+
`.transform()`. A plain schema (`FieldSchema`, `ObjectSchema` since protocol 17
369+
retired their alias-lowering transforms, #3855) is a `ZodObject` and takes
370+
`.extend()` directly. The composition form works in **either** case, and is the
371+
safe default when you don't want to track which is which:
375372

376373
{/* os:check */}
377374
```typescript
378375
import { z } from 'zod';
379376
import { FieldSchema } from '@objectstack/spec/data';
380377

381-
const CustomProps = z.object({ customProperty: z.string().optional() });
378+
// Plain object schema: .extend() works directly. (This line is what pins the
379+
// claim — if FieldSchema ever grows a transform again and becomes a ZodPipe,
380+
// .extend vanishes and this example fails CI instead of the prose going stale.)
381+
const CustomFieldSchema = FieldSchema.extend({
382+
customProperty: z.string().optional(),
383+
});
384+
CustomFieldSchema.parse({ name: 'code', type: 'text', customProperty: 'x' });
382385

386+
// Composition: parse with the built-in schema, validate additions alongside.
387+
// Shape-agnostic — works for plain schemas AND transform-carrying pipes.
388+
const CustomProps = z.object({ customProperty: z.string().optional() });
383389
function parseCustomField(input: unknown) {
384390
return { ...FieldSchema.parse(input), ...CustomProps.parse(input) };
385391
}
386-
387392
parseCustomField({ name: 'code', type: 'text', customProperty: 'x' });
388393
```
389394

390-
If you genuinely need one merged schema object, `FieldSchema.in` is the
391-
`ZodObject` the pipe wraps, so `FieldSchema.in.extend({ … })` builds one — but
392-
the result **skips the transform**, so author-facing sugar the pipe would have
393-
lowered stays raw. Prefer the composition above unless you specifically want the
394-
untransformed shape.
395+
A schema that still lowers author-facing sugar at parse time — `ActionSchema`
396+
(its `requiresFeature``visible` lowering) is one, as of protocol 17 — is a
397+
**`ZodPipe`**, where `.extend` does not exist: prefer the composition form.
398+
Reaching the inner object via `.in.extend({ … })` builds a merged schema but
399+
**skips the transform**, so the sugar the pipe would have lowered stays raw.
400+
401+
A quick check when unsure: `typeof SomeSchema.extend === 'function'` — a pipe
402+
reports `undefined`. This page once asserted the shapes the other way around;
403+
the schemas moved under it within days (#3890#3855), which is why the
404+
load-bearing claim above now lives in a CI-checked block rather than prose.
395405

396406
### Where are the JSON Schemas for IDE autocomplete?
397407

0 commit comments

Comments
 (0)