From 05aea7e8b73859fde8e50302f5d9e4b44044eb22 Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 10 Aug 2026 08:20:23 +0000 Subject: [PATCH] fix(omptype): rejected output-changing schema overrides - Made type.withJsonSchema reject defaults, structural morphs, and pipe steps instead of returning a Type whose runtime output silently remained the original input. - Kept validation-only narrow/filter schemas composable, preserving the legacy Type.Unsafe use case and nested JSON Schema emission. - Added regressions for defaulted and parsed schemas and documented the validation-only contract. Fixes #8143 --- packages/omptype/CHANGELOG.md | 2 +- packages/omptype/src/type.ts | 19 ++++++++++++++----- packages/omptype/test/type.test.ts | 12 ++++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/omptype/CHANGELOG.md b/packages/omptype/CHANGELOG.md index e2e3c51cc..542ae301a 100644 --- a/packages/omptype/CHANGELOG.md +++ b/packages/omptype/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added `type.withJsonSchema(schema, json)`, wrapping a schema so JSON Schema emission yields `json` verbatim even when embedded in objects, arrays, or unions — a `.toJsonSchema()` method override is dropped at nested positions because parents emit a child's IR directly. +- Added `type.withJsonSchema(schema, json)`, wrapping a validation-only schema so JSON Schema emission yields `json` verbatim even when embedded in objects, arrays, or unions — a `.toJsonSchema()` method override is dropped at nested positions because parents emit a child's IR directly. Schemas with defaults or output-changing morphs are rejected to prevent their transformed outputs from being discarded. ## [17.2.10] - 2026-08-06 diff --git a/packages/omptype/src/type.ts b/packages/omptype/src/type.ts index 43780f6c2..eff13bcc2 100644 --- a/packages/omptype/src/type.ts +++ b/packages/omptype/src/type.ts @@ -3677,16 +3677,25 @@ export namespace type { } /** - * Return a schema that validates exactly like `schema` but emits `json` - * verbatim as its JSON Schema — even when embedded in an object, array, or - * union. + * Return a validation-only schema that emits `json` verbatim — even when + * embedded in an object, array, or union. * * A `.toJsonSchema()` method override cannot survive nesting: a parent schema * emits each child's IR directly and never calls the child's method, so the * override silently disappears from the wire schema. This stores the override - * on the IR instead, so structural composition keeps it. + * on the IR instead. + * + * # Errors + * + * Throws when `schema` has a default or output-changing morph/pipe. A refine + * can preserve validation and the input value, but silently discarding a + * transformed output would violate the returned {@link Type}. */ export function withJsonSchema(schema: Type, json: Record): Type { + const internal = schema as unknown as InternalType; + if (internal.hasDefault || hasMorph(internal.ir) || internal[kSteps].some(step => step.kind === "pipe")) { + throw new OmpTypeError("type.withJsonSchema cannot wrap schemas with defaults or output-changing morphs"); + } return makeType( { k: "refine", @@ -3700,7 +3709,7 @@ export namespace type { }, [], {}, - ) as unknown as Type; + ); } } diff --git a/packages/omptype/test/type.test.ts b/packages/omptype/test/type.test.ts index a2050cb5b..a079a66c8 100644 --- a/packages/omptype/test/type.test.ts +++ b/packages/omptype/test/type.test.ts @@ -513,4 +513,16 @@ describe("type.withJsonSchema", () => { expect(object({ mode: "b" })).toEqual({ mode: "b" }); expect(object({ mode: "c" })).toBeInstanceOf(OmpErrors); }); + + it("rejects defaults and output-changing morphs", () => { + expect(() => type.withJsonSchema(type.string.default("fallback"), { type: "string" })).toThrow( + "cannot wrap schemas with defaults or output-changing morphs", + ); + expect(() => + type.withJsonSchema(type("string.integer.parse"), { + type: "string", + pattern: "^[0-9]+$", + }), + ).toThrow("cannot wrap schemas with defaults or output-changing morphs"); + }); });