Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/docs-extend-faq-reversed-rot.md
Original file line number Diff line number Diff line change
@@ -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.
40 changes: 25 additions & 15 deletions content/docs/deployment/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
Loading