Skip to content

feat(errors): StandardSchemaV1 runtime validation + message-as-function (RFC 0001) - #59

Merged
codewizdave merged 18 commits into
mainfrom
docs/rfc-0001-standard-schema-fields
Aug 5, 2026
Merged

feat(errors): StandardSchemaV1 runtime validation + message-as-function (RFC 0001)#59
codewizdave merged 18 commits into
mainfrom
docs/rfc-0001-standard-schema-fields

Conversation

@martyy-code

@martyy-code martyy-code commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Implements RFC 0001. Closes #32. The new API promotes the existing StandardSchemaV1 type to runtime validation and replaces the implicit message template with an explicit function form. The legacy template path keeps working in 1.x and emits a single deprecation warning per call site (silence with DEESSEJS_ERRORS_LEGACY_TEMPLATES=1).

Before / After

Defining an error

const ValidationError = error<{ field: string; reason: string }>({
  name: "ValidationError",
  message: "Field "{field}" is invalid: {reason}",
});

const err = ValidationError({ field: 42, reason: "not a string" });
// err.fields.field is typed string but actually 42. No runtime check.
// err.message is the unrendered template.
const ValidationError = error({
  name: "ValidationError",
  fields: z.object({
    field: z.string(),
    reason: z.string(),
  }),
  message: (data) => `Field "${data.field}" is invalid: ${data.reason}`,
});

const err = ValidationError({ field: 42, reason: "not a string" });
// Throws ArgsValidationError (vendor: "zod") before the message function runs.

Handling the failure

try {
  raise(ValidationError({ field: "" as string }));
} catch (err) {
  // err is a plain Error. No vendor info. No issues. The unrendered
  // template is the only signal that something went wrong.
  if (err instanceof Error) {
    console.log(err.message);
  }
}
try {
  raise(ValidationError({ field: "" }));
} catch (err) {
  if (err instanceof ArgsValidationError) {
    console.log(err.vendor); // "zod" or "valibot" or "arktype"
    console.log(err.issues); // raw validator failure
    console.log(err.source); // factory name
  }
}

What landed

  • Runtime validation: error() now calls ~standard-schema/validate at instantiation when fields is provided. Invalid inputs throw ArgsValidationError carrying the validator's issues and vendor.
  • Message-as-function: when fields is supplied, message must be a function (data) => string receiving the parsed (post-transform) data. The legacy string template still works and is deprecated.
  • ArgsValidationError: new exported class with source, vendor, issues. Re-exported from src/index.ts so consumers can instanceof-check.
  • ErrorFactory.schema removed (decision A from the RFC). The duplication between fields and schema is gone.
  • Test matrix: zod 4, valibot 1, @ark/type 2. CI runs the standard test-run against Node 20, 22, 24.

API surface

  • error(config): name required, fields optional StandardSchemaV1, message optional string | (data) => string, inherits optional.
  • ArgsValidationError(name, issues, vendor): constructor signature.
  • is(err, factory): type guard unchanged.
  • raise(err): unchanged.
  • causes(err): unchanged.
  • StandardSchemaV1: re-exported from @standard-schema/spec.

Test coverage (137 tests across 13 files)

  • tests/error.test.ts, tests/from.test.ts, tests/is.test.ts, tests/raise.test.ts, tests/causes.test.ts: existing unit tests, all pass.
  • tests/standard-schema.test.ts: legacy and standard mode with mock schema.
  • tests/types/error-type.test.ts: expectTypeOf against zod, valibot, arktype. Verifies inference of z.coerce.number(), z.brand, instance core shape, addNote/from chainability, ArgsValidationError contract.
  • tests/edge-cases.test.ts: async validators, throwing validators, non-conformant issues, circular values, call count, legacy template, deprecation gate.
  • tests/snapshots/exports.test.ts: public API surface locked (5 runtime exports: ArgsValidationError, causes, error, is, raise).
  • tests/integration/{zod,valibot,arktype}/vendor.test.ts: cross-vendor parity.
  • tests/integration/contract-parity.test.ts: it.each across the three vendors on ~standard conformance.
  • tests/perf/instantiate.bench.ts: vitest bench for no-fields, schema-validated, and legacy forms. Excluded from test:run via vitest.config.ts; run with pnpm exec vitest bench.

Documentation

  • apps/web/content/docs/{message-templates,fields-schema,error-factory,api-reference}.mdx: updated to the new API. Valibot and ArkType examples corrected (the previous versions imported from non-existent paths).
  • README.md: refreshed to match the shared template; npm page now reflects the new description and SEO fields.
  • apps/web/content/docs/recipes.mdx: still uses the older generic form; left for a follow-up PR to keep this one focused.

Design doc in this PR

  • docs/internal/engineering/rfcs/0001-standard-schema-fields.md: full design doc with Q1-Q4 decisions. The "Decisions locked by review" section replaces the "Open questions" section.

Issues opened for follow-up

CI matrix

Both tests.yml and types.yml run against Node 20, 22, 24 with fail-fast: false. Verified locally:

  • pnpm test:run: 137 passed
  • type-check: clean
  • build: clean
  • apps/web build: 55 static pages generated

All four CI jobs are green on the latest commit.

Out of scope

Checklist

  • RFC 0001 reviewed, Q1-Q4 decisions locked
  • StandardSchemaV1 inference from zod, valibot, arktype
  • Message-as-function receives parsed data
  • ArgsValidationError with vendor + issues
  • Type tests locked with expectTypeOf
  • Cross-vendor integration tests in tests/integration/<vendor>/
  • CI matrix Node 20, 22, 24
  • Documentation aligned to new API
  • Legacy path deprecated with one-shot console.warn

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

martyy-code and others added 17 commits August 5, 2026 11:07
RFC 0001 captures the design decisions for issue #32 before any code lands.
…-as-function

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds cross-vendor parity coverage (zod 4, valibot 1, @ark/type 2) for the new standard-schema mode. All schemas expose '~standard' with version=1 and a vendor string. The runtime dispatch is exercised end-to-end through error().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…type, /contract-parity

Each vendor gets a dedicated file under tests/integration/. The cross-vendor '~standard' contract check is also there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sibling files to integration/vendor-zod.test.ts, completing the one-file-per-vendor split. contract-parity.test.ts checks that each vendor exposes '~standard' with version=1 and a vendor string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…olders

Each validator now has its own folder (zod, valibot, arktype) with a vendor.test.ts. The cross-vendor contract-parity.test.ts stays flat at the integration root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The placeholder test asserted true === true and added no value. The warnLegacy path is covered indirectly by the existing legacy form tests (instantiating any legacy error triggers the warn-once path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Updates four docs pages to the new standard-schema mode:

- message-templates: now leads with the function form (data) => string; legacy template form moved to a 'Legacy' section.

- fields-schema: corrected valibot/arktype imports; documents runtime validation and ArgsValidationError.

- error-factory: documents the function-form message and ArgsValidationError behavior.

- api-reference: rewritten end-to-end. Adds the ArgsValidationError section, message-as-function signature, ErrorInstance methods addNote + from. Removes the broken title-template substitution that was rendering literally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…001)

src/index.ts now exports ArgsValidationError so consumers can instanceof-check.

tests/types/error-type.test.ts locks the public type contract for the new error() API:

- inference from zod, valibot, @ark/type schemas

- preserve transforms and zod brands in the message function

- instance core shape, addNote and from chainability

- legacy template form still type-checks

- ArgsValidationError constructor signature

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Locks the runtime behavior on: async validators (loud error), throwing validators, non-conformant issues, circular values, schema call count, legacy template fallback, empty messages, function-message throws, deprecation gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests/snapshots/exports.test.ts locks the list of named runtime exports and the runtime identity of ArgsValidationError.

tests/perf/instantiate.bench.ts provides an informational vitest bench comparing: no fields, schema-validated factory, legacy template form. Not part of the regular test:run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both tests.yml and types.yml now run on a Node matrix instead of a single version. fail-fast is disabled so we see the full picture on any failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve import

vitest.config.ts now excludes tests/perf/ from the regular test:run so bench files do not break the suite. The bench can still be invoked via pnpm exec vitest bench.

The bench file itself now uses the correct relative path (../../src/index.js).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Acorn parse fails on the embedded curly brace in the table cell. Use a simpler type reference (ReadonlyArray<StandardSchemaV1.Issue>) which avoids the inline object shape that triggered the expression parser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@martyy-code martyy-code changed the title docs(rfc): 0001 - promote StandardSchemaV1 to runtime validation feat(errors): StandardSchemaV1 runtime validation + message-as-function (RFC 0001) Aug 5, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codewizdave
codewizdave merged commit 504afb3 into main Aug 5, 2026
8 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.

[Refactor]: Promote StandardSchemaV1 fields to runtime validation + message-as-function

2 participants