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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<t, i = t>(schema: Type<t, i>, json: Record<string, unknown>): Type<t, i> {
|
||||
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<t, i>(
|
||||
{
|
||||
k: "refine",
|
||||
@@ -3700,7 +3709,7 @@ export namespace type {
|
||||
},
|
||||
[],
|
||||
{},
|
||||
) as unknown as Type<t, i>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user