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
30 changes: 30 additions & 0 deletions .changeset/8345-strict-authoring-face.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'@object-ui/types': minor
---

Publish the derived strict authoring face from `@object-ui/types/zod`
(objectui#8345, under the objectui#5250 ruling — maintainer 2026-09-04,
decision batch #25, option 2: "each node schema gets a derived strict variant;
`objectui validate` and the doc-snippet gates run strict; renderer props keep
the tolerant face unchanged").

**Additive. No existing accept set moves.** `BaseSchema` keeps its
`.passthrough()`, every published mirror keeps the documents it accepts today,
and no consumer in this repository is wired to the new face — wiring
`objectui validate` and the JSON-fence gate is a separate card. What is new:

- `StrictAnyComponentSchema` — the document-root twin of `AnyComponentSchema`,
refusing any undeclared key at any depth with an `unrecognized_keys` issue
that names it.
- `StrictSchemaNodeSchema` — the child-slot twin of `SchemaNodeSchema`.
- `deriveStrictAuthoringSchema(schema, options)` — the derivation itself, so a
consumer can take the strict twin of any schema on the face rather than
writing a second walker.

The twins are **derived**, never hand-written: every reachable object is closed
through unions, discriminated unions, arrays, tuples, records, intersections,
optionals, nullables, defaults, both sides of a pipe, and `z.lazy`. Objects are
cloned by patching a copy of their own def, so `.refine()` and `.superRefine()`
checks survive — a twin rebuilt with `z.object(shape)` would drop them and
under-report. Opaque `custom` / `function` / `transform` validators have no
shape to close and are reported through `onOpaqueShape` rather than skipped.
36 changes: 36 additions & 0 deletions packages/types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@ function renderComponent(schema: AnySchema) {
type ButtonSchema = SchemaByType<'button'>;
```

### The strict authoring face

`@object-ui/types/zod` publishes **two** faces over the same declarations.

- The **rendering face** (`AnyComponentSchema`, `SchemaNodeSchema`, every named
mirror) is tolerant: a node may carry keys the schema does not declare, because
renderer props ride through it.
- The **strict authoring face** is a derived twin that closes every declared
object, at every depth. It is meant for authoring-time checking — validating a
document a person or an agent just wrote — where an undeclared key is far more
likely to be a typo than a renderer prop.

```typescript
import {
AnyComponentSchema,
ButtonSchema,
StrictAnyComponentSchema,
deriveStrictAuthoringSchema,
} from '@object-ui/types/zod';

const document = { type: 'card', childrn: [] }; // note the typo

AnyComponentSchema.safeParse(document).success; // true — the tolerant face
StrictAnyComponentSchema.safeParse(document).success; // false — `unrecognized_keys: ["childrn"]`

// Take the strict twin of any schema on the face:
const StrictButton = deriveStrictAuthoringSchema(ButtonSchema);
```

The twins are derived from the mirrors, never hand-written, so they cannot drift
from them. Strictness here is a property of the parse, not of the declaration:
the derived schema carries the same TypeScript type as the schema it came from.
Opaque `custom` / `function` / `transform` validators have no shape to close;
`deriveStrictAuthoringSchema` reports each one it meets through the optional
`onOpaqueShape` callback.

## Type Categories

### Base Types
Expand Down
Loading
Loading