Skip to content

feat(schema): add optional fallbacks and automatic named references - #230

Merged
andrewzolotukhin merged 5 commits into
developmentfrom
feat/schema-boundaries
Sep 29, 2026
Merged

andrewzolotukhin merged 5 commits into
developmentfrom
feat/schema-boundaries

Conversation

@andrewzolotukhin

@andrewzolotukhin andrewzolotukhin commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Original request

Implement Framework schema-composition improvements against development, with JSDoc, documentation, unit/type tests and minor changesets. Keep optional fallback values and named definitions with local annotations; explicit input/output typing remains removed.

Following review, use automatic references through ordinary modifiers instead of schemaRef. In the latest review cleanup, remove the standalone guide and declaration-inspection test, and consolidate the new website page into existing documentation.

Xpenser and the temporary consumer-experience document are not modified.

What changed

  • Optional/nullable-aware .catch(...) and preprocessor return types accept undefined/null when the schema permits them.
  • Ordinary presence, nullability, annotation and type-only modifiers retain the original canonical named definition. Concrete builder types, fluent methods and extensions remain available.
  • Shape/rule edits, callbacks, defaults, fallbacks, clear methods and extension changes discard inherited names and canonical associations. Explicit schemaName creates a fresh independent definition.
  • JSON Schema Draft 07/2020-12, OpenAPI and AsyncAPI preserve local annotations/nullability and reuse canonical components, including recursion. Independent definitions sharing a name still fail.
  • Removed the proposed wrapper API and its implementation. No explicit input/output APIs or directional components remain.
  • Consolidated fallback/preprocessing and naming guidance into existing README sections and the modifier reference page; API-export guidance lives in existing JSON Schema and OpenAPI/AsyncAPI documentation.
  • Removed BOUNDARIES.md, the unmerged “Schemas Across Boundaries” website page, its route/navigation entries and incoming links. No replacement documentation file or redirect was added.
  • Removed boundaries-docs.test.ts without relocating its emitted-declaration parsing assertions. JSDoc and all behavioral/type/API-document regression tests remain.
  • The latest follow-up changes documentation and removes that one documentation-only test; runtime source and functional tests are unchanged from cbc2f14a.
  • Existing minor changesets cover schema, schema-json and server-openapi. No package version, dependency or lockfile changes.

Consumer example

const User = object({ name: string() }).schemaName('User');
const History = object({
    current: User,
    previous: User.optional().nullable().describe('Previous user')
});

const PatchUser = User.partial(); // unnamed, changed shape
const PublicUser = User.addProp('id', number()).schemaName('PublicUser');

Reasoning

Ordinary immutable builders provide the desired consumer experience without an extra wrapper. Canonical-reference metadata supports exporters without changing runtime validation.

Naming intentionally changes for shape/rule edits: apply schemaName after those edits when the result needs a stable component name. Use-site modifiers retain canonical identity; independently named definitions still conflict.

Legacy optional schemas continue accepting null at runtime even when inference excludes it. Existing documentation now explains why .optional().catch(undefined) leaves null unchanged and how to normalize explicitly.

Documentation belongs alongside the corresponding APIs rather than in a duplicate guide/page. The declaration-inspection test only asserted JSDoc presence, not feature behavior; it is removed while retaining JSDoc, runtime/type coverage and normal TypeDoc generation.

Blog post

Skipped: library/API review and documentation cleanup, not an application-facing feature.

Screenshots / preview evidence

No Framework PR preview environment is configured, so preview screenshots/browser QA are unavailable. Both existing documentation sites were typechecked; moved examples were exercised against built packages, and guidance placement and links were checked locally. TypeDoc HTML was generated into temporary output without modifying tracked snapshots.

Validation

  • npm run lint
  • npm run build — 21 successful package tasks
  • npm run test — 4,347 passed, 188 files, no type errors; one documentation-only test removed
  • npm run typecheck:schema-site
  • npm run typecheck:docs-site
  • TypeDoc generation — zero errors, 241 existing warnings
  • Moved examples checked against built packages, including Draft 07/2020-12 reference composition
  • Guidance is nested in existing reference sections; replacement links target existing anchors
  • No remaining links/imports/route registrations for the removed guide/page
  • npm pack --dry-run — schema (101 files), schema-json (9), server-openapi (15); READMEs included, removed guide/builders/tests excluded
  • Runtime source and behavioral/type tests unchanged by the documentation follow-up
  • git diff --check
  • Existing minor changeset retained
  • GitHub: Lint, Build & Test (Node 24) — passed on 89abbf53
  • GitHub: PostgreSQL Query Integration — passed on 89abbf53
  • Preview/browser e2e: skipped; no configured Framework PR environment.
  • SigNoz: skipped; no Framework PR telemetry environment.
  • Telegram: skipped; no preview/deployed URL exists for this library PR, which the notifier requires.

Retained regression coverage

Canonical identity across all concrete builders; direct reuse and chained modifiers; explicit renaming and strict independent-name conflicts; structural/rule/extension detachment; optional fallbacks and legacy null behavior; defaults and async callbacks; nested selectors/errors and type inference; JSON Schema/OpenAPI/AsyncAPI annotations, mixed inline/named graphs and recursion.

@andrewzolotukhin andrewzolotukhin changed the title feat(schema): compose schemas across input and output boundaries feat(schema): add optional-aware fallbacks and named references Sep 28, 2026
Comment thread libs/schema/package.json Outdated
"files": [
"dist"
"dist",
"BOUNDARIES.md"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we add this? remove it from here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cbc2f14. Removed BOUNDARIES.md from files; the package ships dist as before. The guide remains in the repository, and published README links now point to that repository copy. Verified with npm pack --dry-run that the guide is excluded.

Comment on lines +67 to +68
const user = object({ address: schemaRef(address) }).schemaName('User');
const root = object({ user: schemaRef(user) });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't apply modifiers (like .optional(), etc) there is no need to use schemaRef wrapping. We should be able to use a plain schema in this case. Ideally I would not have schemaRef at all, but I don't know if it's possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cbc2f14 using the agreed automatic-reference approach. schemaRef and ReferenceSchemaBuilder are removed completely. Use the plain constant directly, or ordinary calls such as User.optional().nullable().describe(...); these preserve its canonical definition without changing the concrete builder. Property/constraint/callback/default/fallback/extension changes discard the inherited name and canonical association, and .schemaName('NewName') establishes a new independent definition. Docs, JSDoc and regression tests cover the policy, name conflicts, JSON Schema/OpenAPI/AsyncAPI and recursion. Local gates pass (4,348 tests); GitHub checks are running.

@andrewzolotukhin andrewzolotukhin changed the title feat(schema): add optional-aware fallbacks and named references feat(schema): add optional fallbacks and automatic named references Sep 29, 2026
Comment thread libs/schema/BOUNDARIES.md Outdated
Comment on lines +3 to +33
`InferType<S>` continues to mean validated output;
existing builder generic arguments and legacy preprocessors keep their meaning.

## Optional fallbacks

Before, consumers needed a separate safeParse helper for each optional scalar.
Now a fallback can use the optional or nullable schema's full output type:

```ts
import { boolean, object, string } from '@cleverbrush/schema';

const optionalText = string().optional().catch(undefined);
const ExternalRecord = object({
name: optionalText,
enabled: boolean().optional().catch(() => undefined)
});
ExternalRecord.parse({ name: 42, enabled: 'unknown' });
// { name: undefined, enabled: undefined }
```

Fallbacks are opt-in. A malformed required root object still fails. Arrays do
not silently discard invalid elements. A fallback factory runs only on failure.

**Compatibility limitation:** legacy optional schemas also accept `null` at
runtime, even though their inferred type does not include it. This release does
not change that behavior. Consequently, `.optional().catch(undefined)` leaves
`null` unchanged. Normalize it explicitly when the application requires this:

```ts
const normalizedText = string().optional()
.addPreprocessor(value => value == null ? undefined : value)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this file at all, just update corresponding documents/readme/etc instead of creation of a new file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 89abbf5. Removed BOUNDARIES.md and moved its guidance into the existing README fallback/schemaName/API-export sections. Following the agreed cleanup, also removed the new “Schemas Across Boundaries” website page and its route/navigation entries; the content now lives in the existing modifier, JSON Schema and OpenAPI reference pages. All links point to existing sections, with no replacement guide file.

Comment thread libs/schema/src/boundaries-docs.test.ts Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need these tests at all, they look weird, or if we absolutely need them, let's add them to existing test files.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 89abbf5. Removed boundaries-docs.test.ts entirely instead of moving the generated-declaration/JSDoc inspection assertions elsewhere. JSDoc and every behavioral, type-inference and API-document regression test remain unchanged. Local validation passes: 4,347 tests in 188 files, build, lint, both site typechecks, and TypeDoc generation (zero errors). GitHub checks are running on the follow-up commit.

@andrewzolotukhin
andrewzolotukhin merged commit f9eb11b into development Sep 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant