typespec-http-zod 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/src/api.d.ts +2 -0
- package/dist/src/api.js +132 -45
- package/dist/src/constraints.d.ts +7 -6
- package/dist/src/constraints.js +7 -6
- package/dist/src/lib.d.ts +43 -1
- package/dist/src/lib.js +38 -0
- package/dist/src/types.js +28 -10
- package/dist/src/zod.js +53 -9
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -148,6 +148,8 @@ carries the whole id, so `op readWidget` in `namespace Widgets` becomes `Widgets
|
|
|
148
148
|
- [Reference](docs/reference.md): every option, every diagnostic, and the known limits.
|
|
149
149
|
- [Oracles](docs/oracles.md): every artefact this emitter produces, and what compares it to the
|
|
150
150
|
thing it has to agree with.
|
|
151
|
+
- [Releasing](docs/releasing.md): why this package publishes first, and how to rehearse a
|
|
152
|
+
two-package release against a local registry.
|
|
151
153
|
|
|
152
154
|
## Licence
|
|
153
155
|
|
package/dist/src/api.d.ts
CHANGED
|
@@ -373,6 +373,8 @@ export interface ResolvedServiceOptions {
|
|
|
373
373
|
readonly contractsOutputDir: string | undefined;
|
|
374
374
|
readonly contractsPackage: string | undefined;
|
|
375
375
|
readonly sealObjectSchemas: boolean;
|
|
376
|
+
/** Whether every emitted validator is wrapped in `z.compile()`. See the option's own docblock. */
|
|
377
|
+
readonly compileSchemas: boolean;
|
|
376
378
|
readonly keyVocabularies: readonly string[];
|
|
377
379
|
readonly runtimeModule: string;
|
|
378
380
|
/** The command that regenerates, for the banner. `undefined` keeps the generic line. */
|
package/dist/src/api.js
CHANGED
|
@@ -680,7 +680,9 @@ function multipartSchemaOf(body, registry) {
|
|
|
680
680
|
: registry.expressionFor(type);
|
|
681
681
|
// `multi` is `HttpPart<T>[]` - the same part repeated, which arrives as an array.
|
|
682
682
|
const value = part.multi === true ? `z.array(${inner})` : inner;
|
|
683
|
-
|
|
683
|
+
// `.exactOptional()` for the same reason as a model property: `parseBody` yields present keys
|
|
684
|
+
// only, so an absent part is an absent key rather than an explicit `undefined`.
|
|
685
|
+
return `\t${objectKey(part.name ?? "")}: ${part.optional === true ? `${value}.exactOptional()` : value},`;
|
|
684
686
|
});
|
|
685
687
|
const shape = entries.length === 0 ? "z.object({})" : `z.object({\n${entries.join("\n")}\n})`;
|
|
686
688
|
/**
|
|
@@ -1266,7 +1268,26 @@ function renderExternalImports(externals, ...bodies) {
|
|
|
1266
1268
|
* validate a request or a response against, and every oracle that graded those validators had to read
|
|
1267
1269
|
* a server to find them. See {@link nameRouteSchemas}.
|
|
1268
1270
|
*/
|
|
1269
|
-
|
|
1271
|
+
/**
|
|
1272
|
+
* **`z.compile(...)` around a validator, when `compile-schemas` asks for it.**
|
|
1273
|
+
*
|
|
1274
|
+
* Zod walks the schema once and emits a flat, loop-free function through `new Function`, falling back
|
|
1275
|
+
* to the ordinary parser for anything it cannot handle. Measured on zod 4.5.2 before this was written:
|
|
1276
|
+
* `z.toJSONSchema` output is byte-identical, `_zod.def` is unchanged and still readable, and verdicts
|
|
1277
|
+
* AND parsed output match for every construct this emitter writes - including `z.preprocess`,
|
|
1278
|
+
* `.exactOptional()` and a `z.lazy()` cycle. So the two oracles that read a schema rather than run it
|
|
1279
|
+
* are unaffected, and the wrapper is transparent.
|
|
1280
|
+
*
|
|
1281
|
+
* **A cyclic declaration is wrapped too, and that was worth checking rather than assuming.** The
|
|
1282
|
+
* emitted form names the const being defined from inside its own `z.lazy()` body, and this repository
|
|
1283
|
+
* has already shipped a break of exactly that shape - `.strict()` reads `shape` eagerly and throws
|
|
1284
|
+
* `Cannot access 'X' before initialization` during module initialisation. Measured on the emitted
|
|
1285
|
+
* shape: `z.compile()` does not force the lazy body, and the declaration parses.
|
|
1286
|
+
*/
|
|
1287
|
+
function compiled(expression, on) {
|
|
1288
|
+
return on ? `z.compile(${expression})` : expression;
|
|
1289
|
+
}
|
|
1290
|
+
function renderSchemas(registry, externals, routeDeclarations, runtimeModule, compileSchemas) {
|
|
1270
1291
|
const declarations = registry.declarations().map((d) => {
|
|
1271
1292
|
/**
|
|
1272
1293
|
* **A declaration on a CYCLE cannot take its type from `z.infer`, because that is the loop.**
|
|
@@ -1277,13 +1298,53 @@ function renderSchemas(registry, externals, routeDeclarations, runtimeModule) {
|
|
|
1277
1298
|
* with no cycle emits byte-identical output to before.
|
|
1278
1299
|
*/
|
|
1279
1300
|
if (d.structural === undefined) {
|
|
1280
|
-
return `\nexport const ${d.identifier} = ${d.source};\nexport type ${d.typeName} =
|
|
1301
|
+
return `\nexport const ${d.identifier} = ${compiled(d.source, compileSchemas)};\nexport type ${d.typeName} = z.infer<typeof ${d.identifier}>;\n`;
|
|
1281
1302
|
}
|
|
1282
1303
|
const declared = d.structural.startsWith("{")
|
|
1283
1304
|
? `export interface ${d.typeName} ${d.structural}`
|
|
1284
1305
|
: `export type ${d.typeName} = ${d.structural};`;
|
|
1285
|
-
|
|
1286
|
-
|
|
1306
|
+
/**
|
|
1307
|
+
* **BOTH of Zod's type parameters, and naming only the first left the second as `unknown`.**
|
|
1308
|
+
*
|
|
1309
|
+
* `z.ZodType<T>` is `ZodType<Output = T, Input = unknown>`, so a schema annotated with one
|
|
1310
|
+
* parameter reports `z.input<typeof x>` as `unknown` - and every schema that REFERENCES it
|
|
1311
|
+
* inherits that. Measured: `Identical<Declared<z.input<typeof nodeSchema>>, Contracts.Node>`
|
|
1312
|
+
* failed on `branch?: unknown | undefined` against `branch?: Branch | undefined`, in a fixture
|
|
1313
|
+
* whose two artefacts agree about every property. The wire assertion pairs `z.input` because
|
|
1314
|
+
* that is what a caller supplies, so an unannotated input parameter takes the whole cyclic
|
|
1315
|
+
* corner of the emitter out of that comparison.
|
|
1316
|
+
*
|
|
1317
|
+
* **One structural type serves both positions, and for a defaulted property on a cycle it is
|
|
1318
|
+
* the INPUT shape that both get.** A default is the only construct where a schema's input and
|
|
1319
|
+
* output types differ, and a cyclic declaration has exactly one type written out to name - the
|
|
1320
|
+
* loop is what stops `z.infer` being usable at all. Claiming the input shape in the output
|
|
1321
|
+
* position is the PERMISSIVE direction: every value that arrives still satisfies it, a reader
|
|
1322
|
+
* simply learns less than it could about a defaulted property. Claiming the output shape in
|
|
1323
|
+
* the input position is the restrictive one, and it is the defect this release exists to
|
|
1324
|
+
* remove - a caller told to supply what the document says they may omit.
|
|
1325
|
+
*
|
|
1326
|
+
* So the limitation is confined to this: a model on a cycle carrying a defaulted optional
|
|
1327
|
+
* property exports a type from `schemas.gen.ts` that is weaker than what arrives, by exactly
|
|
1328
|
+
* that property. `typespec-hono` derives its handler input from `z.infer` of the schema rather
|
|
1329
|
+
* than from this type, so no handler signature is affected.
|
|
1330
|
+
*/
|
|
1331
|
+
const annotation = d.annotated === true ? `: z.ZodType<${d.typeName}, ${d.typeName}>` : "";
|
|
1332
|
+
/**
|
|
1333
|
+
* **A deferred declaration is NEVER compiled, and this was measured rather than reasoned.**
|
|
1334
|
+
*
|
|
1335
|
+
* `z.lazy()` is on Zod's own list of constructs the compiler cannot handle, and here the body
|
|
1336
|
+
* forward-references a `const` declared further down the module. Wrapping it emitted output that
|
|
1337
|
+
* loaded fine and then threw `Cannot read properties of undefined (reading '_zod')` on the first
|
|
1338
|
+
* parse - the walk captures the reference while it is still in the temporal dead zone, so the
|
|
1339
|
+
* failure surfaces at request time rather than at import.
|
|
1340
|
+
*
|
|
1341
|
+
* **Keyed on `annotated`, the fact the registry already holds** - it is set exactly when the
|
|
1342
|
+
* declaration is deferred, which is exactly when `z.lazy()` is written - rather than on the
|
|
1343
|
+
* shape of the emitted text. A model on a cycle whose back edge sits on a GETTER is a different
|
|
1344
|
+
* case and is compiled like any other: measured, it parses, because a getter is not resolved
|
|
1345
|
+
* until something reads it.
|
|
1346
|
+
*/
|
|
1347
|
+
return `\n${declared}\nexport const ${d.identifier}${annotation} = ${compiled(d.source, compileSchemas && d.annotated !== true)};\n`;
|
|
1287
1348
|
});
|
|
1288
1349
|
/**
|
|
1289
1350
|
* **Written only when something in the file names it**, like the `ResponseArm` import below and for
|
|
@@ -1302,37 +1363,24 @@ function renderSchemas(registry, externals, routeDeclarations, runtimeModule) {
|
|
|
1302
1363
|
* rendered content for the same reason.
|
|
1303
1364
|
*/
|
|
1304
1365
|
/**
|
|
1305
|
-
* **`Exact
|
|
1306
|
-
*
|
|
1307
|
-
* `z.infer` of `.optional()` gives `p?: T | undefined`, which under `exactOptionalPropertyTypes`
|
|
1308
|
-
* admits an explicitly-undefined property. **JSON has no `undefined`**, and the document says the
|
|
1309
|
-
* same thing by leaving the property out of `required`, so the inferred type is wider than the
|
|
1310
|
-
* wire and a consumer reading a payload had to strip it at the boundary.
|
|
1366
|
+
* **`Exact<>` lived here until `0.24.0`, and `z.exactOptional()` replaced it.**
|
|
1311
1367
|
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
* was "fixed" to agree: `(ctx, input) => ok(input)` - return what you were given - stopped
|
|
1317
|
-
* compiling.
|
|
1368
|
+
* It was a mapped type over `z.infer` that stripped `| undefined` off every optional property,
|
|
1369
|
+
* because `.optional()` infers `p?: T | undefined` and **JSON has no `undefined`** - the document
|
|
1370
|
+
* says "absent or a value" by leaving the property out of `required`, so the inferred type was
|
|
1371
|
+
* wider than both the document and the wire.
|
|
1318
1372
|
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1373
|
+
* The reasoning was right and it was applied to only one of the two artefacts. The VALIDATOR went
|
|
1374
|
+
* on accepting `{ p: undefined }`, so one program published a type refusing a value beside a
|
|
1375
|
+
* validator admitting it, compared by nothing. Emitting `.exactOptional()` makes `z.infer` produce
|
|
1376
|
+
* `p?: T` directly - at every depth, which a shallow mapped type never reached - so the narrowing
|
|
1377
|
+
* happens once, in the schema, and this helper has nothing left to do.
|
|
1323
1378
|
*
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1379
|
+
* The contract types in `requests.gen.ts` still keep `?: T | undefined` on purpose: they are the
|
|
1380
|
+
* floor a PRODUCER supplies, and `{ p: undefined }` serialises identically to omitting `p`, so
|
|
1381
|
+
* refusing it there would cost every producer a conditional spread and buy the wire nothing.
|
|
1382
|
+
* Narrowing THAT surface was tried in `0.19.0` and reverted - it broke `(ctx, input) => ok(input)`.
|
|
1328
1383
|
*/
|
|
1329
|
-
const EXACT_HELPER = `
|
|
1330
|
-
/** An inferred type narrowed to what JSON can carry: an optional property is absent or a value. */
|
|
1331
|
-
type Exact<T> = { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } & {
|
|
1332
|
-
[K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
|
|
1333
|
-
};
|
|
1334
|
-
`;
|
|
1335
|
-
const usesExact = declarations.some((d) => d.includes("Exact<"));
|
|
1336
1384
|
const usesZod = new Set([...declarations, ...routeDeclarations].join("").match(/[A-Za-z_$][\w$]*/g) ?? []).has("z");
|
|
1337
1385
|
const parts = [generatedBanner()];
|
|
1338
1386
|
if (usesZod)
|
|
@@ -1349,8 +1397,6 @@ type Exact<T> = { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } &
|
|
|
1349
1397
|
parts.push(`import type { ResponseArm } from ${JSON.stringify(runtimeModule)};\n`);
|
|
1350
1398
|
}
|
|
1351
1399
|
parts.push(renderExternalImports(externals, [...declarations, ...routeDeclarations].join("")));
|
|
1352
|
-
if (usesExact)
|
|
1353
|
-
parts.push(EXACT_HELPER);
|
|
1354
1400
|
parts.push(declarations.join(""));
|
|
1355
1401
|
if (routeDeclarations.length > 0)
|
|
1356
1402
|
parts.push(`\n${routeDeclarations.join("\n")}\n`);
|
|
@@ -1367,7 +1413,7 @@ type Exact<T> = { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } &
|
|
|
1367
1413
|
const isIdentifier = (expression) => /^[A-Za-z_$][\w$]*$/.test(expression);
|
|
1368
1414
|
const schemaConst = (operationId, suffix) => `${operationId}${suffix}`;
|
|
1369
1415
|
/** `[the name to refer to it by, the `const` line to emit]` - the line is absent when none is needed. */
|
|
1370
|
-
function namedSchema(operationId, suffix, expression) {
|
|
1416
|
+
function namedSchema(operationId, suffix, expression, compileSchemas) {
|
|
1371
1417
|
if (expression === undefined)
|
|
1372
1418
|
return { name: undefined, declaration: undefined };
|
|
1373
1419
|
if (isIdentifier(expression))
|
|
@@ -1384,7 +1430,7 @@ function namedSchema(operationId, suffix, expression) {
|
|
|
1384
1430
|
* Exporting also gives an application the request shape by name, which is what the per-operation
|
|
1385
1431
|
* type aliases will be built from.
|
|
1386
1432
|
*/
|
|
1387
|
-
return { name, declaration: `export const ${name} = ${expression};` };
|
|
1433
|
+
return { name, declaration: `export const ${name} = ${compiled(expression, compileSchemas)};` };
|
|
1388
1434
|
}
|
|
1389
1435
|
/**
|
|
1390
1436
|
* Which `verb path` slots carry more than one operation.
|
|
@@ -1426,13 +1472,13 @@ function sharedSlots(routes) {
|
|
|
1426
1472
|
* Declaring them here dissolves all three: the library mints the names AND emits them, and a server
|
|
1427
1473
|
* emitter imports what it is told rather than agreeing about it.
|
|
1428
1474
|
*/
|
|
1429
|
-
function nameRouteSchemas(routes) {
|
|
1475
|
+
function nameRouteSchemas(routes, compileSchemas) {
|
|
1430
1476
|
const shared = sharedSlots(routes);
|
|
1431
1477
|
const declarations = [];
|
|
1432
1478
|
const names = new Map();
|
|
1433
1479
|
for (const route of routes) {
|
|
1434
1480
|
const declare = (suffix, expression) => {
|
|
1435
|
-
const resolved = namedSchema(route.operationId, suffix, expression);
|
|
1481
|
+
const resolved = namedSchema(route.operationId, suffix, expression, compileSchemas);
|
|
1436
1482
|
if (resolved.declaration !== undefined)
|
|
1437
1483
|
declarations.push(resolved.declaration);
|
|
1438
1484
|
return resolved.name;
|
|
@@ -1795,8 +1841,26 @@ ${responseLookup}
|
|
|
1795
1841
|
* both look right in isolation. That happened during this emitter's first run: a vocabulary in a
|
|
1796
1842
|
* query-parameter position was inlined by one walk and referenced by the other.
|
|
1797
1843
|
*
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1844
|
+
* **`z.input`, not `z.infer`, and the difference is the whole caller-side question.** `z.infer` is
|
|
1845
|
+
* the OUTPUT type - what a handler receives, after `.default(...)` has fired. These contract types
|
|
1846
|
+
* are the floor a PRODUCER supplies, and for a request the producer is the caller, who has not run
|
|
1847
|
+
* the validator. `z.input` is what the validator ACCEPTS, which is what a request contract states
|
|
1848
|
+
* and what the document's `required` list says. Measured on zod 4.4.3: `.default()` is the only
|
|
1849
|
+
* construct this emitter emits where the two differ - `.optional()`, `.nullable()` and every
|
|
1850
|
+
* constraint infer identically on both sides.
|
|
1851
|
+
*
|
|
1852
|
+
* Once `default-on-required-property` has removed the incoherent spelling, `z.input` and the
|
|
1853
|
+
* document's `required` list agree property for property, so this assertion cross-checks that rule
|
|
1854
|
+
* rather than merely restating one side of it.
|
|
1855
|
+
*
|
|
1856
|
+
* **One measured exception, which does not reach here.** `.refine()` with a type predicate narrows
|
|
1857
|
+
* the OUTPUT only - zod types it `this & ZodType<R, core.input<this>>` - so `z.input` of a
|
|
1858
|
+
* `HttpPart<File>` part is `unknown` where `z.infer` is the narrowed file shape. A multipart body's
|
|
1859
|
+
* contract type is an inline object literal rather than a named declaration, so it is never in the
|
|
1860
|
+
* paired set below; if that ever changes, this is the pairing that breaks first.
|
|
1861
|
+
*
|
|
1862
|
+
* `Equals` here, not assignability: the two describe one shape, and a property present on one side
|
|
1863
|
+
* and absent from the other is exactly the drift this exists to catch.
|
|
1800
1864
|
*/
|
|
1801
1865
|
function renderWireAssertions(schemas, types, entries, contractsPackage) {
|
|
1802
1866
|
// The assertions pair the emitted Zod against the request TYPES, which live in the contracts
|
|
@@ -1818,7 +1882,7 @@ function renderWireAssertions(schemas, types, entries, contractsPackage) {
|
|
|
1818
1882
|
}
|
|
1819
1883
|
if (paired.length === 0)
|
|
1820
1884
|
return undefined;
|
|
1821
|
-
const assertions = paired.map((pair) => `export type Assert${pair.name} = MustHold<Identical<Declared<z.
|
|
1885
|
+
const assertions = paired.map((pair) => `export type Assert${pair.name} = MustHold<Identical<Declared<z.input<typeof ${pair.identifier}>>, Contracts.${pair.name}>>;`);
|
|
1822
1886
|
return `${generatedBanner()}
|
|
1823
1887
|
import type { z } from "zod";
|
|
1824
1888
|
import type * as Contracts from "${contractsPackage}";
|
|
@@ -1868,6 +1932,26 @@ type Identical<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends
|
|
|
1868
1932
|
* dropped from a shape that has other keys, and kept - with its value type still mapped - from one
|
|
1869
1933
|
* that does not.
|
|
1870
1934
|
*/
|
|
1935
|
+
/**
|
|
1936
|
+
* \u26a0\ufe0f **An optional property is \`?: T\` here and \`?: T | undefined\` in the contract type, and that is
|
|
1937
|
+
* deliberate on both sides.**
|
|
1938
|
+
*
|
|
1939
|
+
* The validator emits \`.exactOptional()\`, so what ARRIVES is absent or a value - JSON cannot carry an
|
|
1940
|
+
* \`undefined\` and the document says exactly that by leaving the property out of \`required\`. The
|
|
1941
|
+
* contract type is the floor a PRODUCER supplies, and \`{ p: undefined }\` serialises identically to
|
|
1942
|
+
* omitting \`p\`, so refusing it would cost every producer a conditional spread and buy the wire
|
|
1943
|
+
* nothing. Narrowing THAT surface was tried in \`0.19.0\` and reverted: it broke
|
|
1944
|
+
* \`(ctx, input) => ok(input)\`.
|
|
1945
|
+
*
|
|
1946
|
+
* So the two disagree on exactly one axis, on purpose, and this widens the inferred side to the
|
|
1947
|
+
* producer floor before comparing - the same job \`Declared\` does for openness, in the same pass
|
|
1948
|
+
* rather than a second deep one. Every other kind of drift still fails: a property that changes type,
|
|
1949
|
+
* appears, disappears or changes optionality is untouched by this.
|
|
1950
|
+
*/
|
|
1951
|
+
type Supplied<T, K extends keyof T> = {} extends Pick<T, K>
|
|
1952
|
+
? Declared<T[K]> | undefined
|
|
1953
|
+
: Declared<T[K]>;
|
|
1954
|
+
|
|
1871
1955
|
type DeclaredKeyOf<T> = keyof {
|
|
1872
1956
|
[K in keyof T as string extends K
|
|
1873
1957
|
? never
|
|
@@ -1886,7 +1970,9 @@ type Declared<T> = T extends (...args: never[]) => unknown
|
|
|
1886
1970
|
: readonly Declared<Element>[]
|
|
1887
1971
|
: T extends object
|
|
1888
1972
|
? [DeclaredKeyOf<T>] extends [never]
|
|
1889
|
-
?
|
|
1973
|
+
? // A shape with no declared keys is a dictionary: it has no optional property to widen,
|
|
1974
|
+
// and an index signature satisfies \`{} extends Pick<T, K>\` for every value type.
|
|
1975
|
+
{ [K in keyof T]: Declared<T[K]> }
|
|
1890
1976
|
: {
|
|
1891
1977
|
[K in keyof T as string extends K
|
|
1892
1978
|
? never
|
|
@@ -1894,7 +1980,7 @@ type Declared<T> = T extends (...args: never[]) => unknown
|
|
|
1894
1980
|
? never
|
|
1895
1981
|
: symbol extends K
|
|
1896
1982
|
? never
|
|
1897
|
-
: K]:
|
|
1983
|
+
: K]: Supplied<T, K>;
|
|
1898
1984
|
}
|
|
1899
1985
|
: T;
|
|
1900
1986
|
|
|
@@ -2032,6 +2118,7 @@ export async function emitHttpZod(context, wrapper = {}) {
|
|
|
2032
2118
|
contractsOutputDir: perService?.["contracts-output-dir"] ?? options["contracts-output-dir"],
|
|
2033
2119
|
contractsPackage: perService?.["contracts-package"] ?? options["contracts-package"],
|
|
2034
2120
|
sealObjectSchemas: perService?.["seal-object-schemas"] ?? options["seal-object-schemas"] ?? false,
|
|
2121
|
+
compileSchemas: perService?.["compile-schemas"] ?? options["compile-schemas"] ?? false,
|
|
2035
2122
|
keyVocabularies: perService?.["key-vocabularies"] ?? options["key-vocabularies"] ?? [],
|
|
2036
2123
|
runtimeModule: perService?.["runtime-module"] ?? options["runtime-module"] ?? defaultRuntimeModule,
|
|
2037
2124
|
regenerateHint: perService?.["regenerate-hint"] ?? options["regenerate-hint"],
|
|
@@ -2088,10 +2175,10 @@ export async function emitHttpZod(context, wrapper = {}) {
|
|
|
2088
2175
|
* anything is written - and it has to happen HERE rather than in a server emitter, or the
|
|
2089
2176
|
* validators it produces never reach this package's own output.
|
|
2090
2177
|
*/
|
|
2091
|
-
const { names, declarations } = nameRouteSchemas(routes);
|
|
2178
|
+
const { names, declarations } = nameRouteSchemas(routes, resolved.compileSchemas);
|
|
2092
2179
|
await emitFile(context.program, {
|
|
2093
2180
|
path: resolvePath(outputDir, "schemas.gen.ts"),
|
|
2094
|
-
content: renderSchemas(registry, externals, declarations, resolved.runtimeModule),
|
|
2181
|
+
content: renderSchemas(registry, externals, declarations, resolved.runtimeModule, resolved.compileSchemas),
|
|
2095
2182
|
});
|
|
2096
2183
|
if (resolved.contractsOutputDir !== undefined) {
|
|
2097
2184
|
const types = new TypeRegistry(context.program);
|
|
@@ -6,12 +6,13 @@ import { type Program, type Type } from "@typespec/compiler";
|
|
|
6
6
|
* over: the spec author writes standard TypeSpec rather than a dialect, and `@typespec/openapi3`
|
|
7
7
|
* documents them without any help from us.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* other
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* **This package ships no decorators of its own, and the two it used to have are gone.** `@refine`
|
|
10
|
+
* was an arbitrary predicate, which a type language has no way to say and therefore no document can
|
|
11
|
+
* publish, so a validator enforcing it enforced something no caller could read. `@trimmed` was the
|
|
12
|
+
* other: `.trim()` is a transform, and transforming a value before validating it makes the runtime
|
|
13
|
+
* accept inputs the published contract rejects. The spec states `@pattern` instead, which openapi3
|
|
14
|
+
* publishes. `src/tsp-index.ts` exports `$lib` and no `$decorators`, and `vocabulary.test.ts`
|
|
15
|
+
* asserts that no emitted file carries a `.refine(` outside the one multipart-file carve-out.
|
|
15
16
|
*/
|
|
16
17
|
export declare function applyConstraints(program: Program, expression: string, target: Type): string;
|
|
17
18
|
/**
|
package/dist/src/constraints.js
CHANGED
|
@@ -6,12 +6,13 @@ import { getMaxItems, getMaxLength, getMaxValue, getMinItems, getMinLength, getM
|
|
|
6
6
|
* over: the spec author writes standard TypeSpec rather than a dialect, and `@typespec/openapi3`
|
|
7
7
|
* documents them without any help from us.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* other
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* **This package ships no decorators of its own, and the two it used to have are gone.** `@refine`
|
|
10
|
+
* was an arbitrary predicate, which a type language has no way to say and therefore no document can
|
|
11
|
+
* publish, so a validator enforcing it enforced something no caller could read. `@trimmed` was the
|
|
12
|
+
* other: `.trim()` is a transform, and transforming a value before validating it makes the runtime
|
|
13
|
+
* accept inputs the published contract rejects. The spec states `@pattern` instead, which openapi3
|
|
14
|
+
* publishes. `src/tsp-index.ts` exports `$lib` and no `$decorators`, and `vocabulary.test.ts`
|
|
15
|
+
* asserts that no emitted file carries a `.refine(` outside the one multipart-file carve-out.
|
|
15
16
|
*/
|
|
16
17
|
export function applyConstraints(program, expression, target) {
|
|
17
18
|
let result = expression;
|
package/dist/src/lib.d.ts
CHANGED
|
@@ -42,6 +42,38 @@ export interface EmitterOptions {
|
|
|
42
42
|
* stating it outright rather than an emitter option deciding.
|
|
43
43
|
*/
|
|
44
44
|
"seal-object-schemas"?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether every emitted validator is wrapped in `z.compile()`.
|
|
47
|
+
*
|
|
48
|
+
* **A pre-compiled schema parses the same values to the same results, only faster.** Zod walks the
|
|
49
|
+
* schema once and emits a flat, loop-free function through `new Function`, then falls back to the
|
|
50
|
+
* ordinary parser for anything it cannot handle - so errors, parsed output and JSON Schema
|
|
51
|
+
* serialisation are unchanged. Measured on an emitted five-property model at zod 4.5.2:
|
|
52
|
+
* `safeParse` goes from 371 ns to 51 ns.
|
|
53
|
+
*
|
|
54
|
+
* **Off by default, because it is a trade rather than a free win.** Compilation happens at module
|
|
55
|
+
* scope, so it costs startup time in proportion to the number of schemas, and buys nothing until
|
|
56
|
+
* enough requests arrive to pay that back. Measured on two emitted modules at zod 4.5.2: 25
|
|
57
|
+
* declarations in 3.3 ms and 73 in 5.7 ms, so 0.08-0.13 ms each depending on how much the schema
|
|
58
|
+
* does.
|
|
59
|
+
*
|
|
60
|
+
* **Measured at scale rather than extrapolated:** a generated 580-operation service (58 models,
|
|
61
|
+
* 1218 declarations, 638 of them compiled) evaluates its schema module in ~93 ms uncompiled and
|
|
62
|
+
* ~132 ms compiled - so about **40 ms of extra startup**, roughly a tenth of a Cloudflare Worker's
|
|
63
|
+
* 400 ms startup budget. It boots and serves on `workerd` at that size with the fast path live. It also needs
|
|
64
|
+
* `new Function`, which a CSP or no-eval environment refuses; Zod degrades to the uncompiled
|
|
65
|
+
* schema there rather than throwing, so setting this is never unsafe, merely sometimes pointless.
|
|
66
|
+
*
|
|
67
|
+
* **On Cloudflare Workers this is the only route to a compiled schema.** `new Function` is
|
|
68
|
+
* permitted during a Worker's STARTUP phase (`allow_eval_during_startup`, the default from
|
|
69
|
+
* compatibility date 2025-06-01) and module scope is startup, so wrapping here works. Zod's own
|
|
70
|
+
* `import "zod/compile"` does not: it compiles on first PARSE, which happens inside a request,
|
|
71
|
+
* where the runtime refuses. Only the emitter owns the module scope these schemas are built in.
|
|
72
|
+
*
|
|
73
|
+
* A recursive model emits `z.lazy()`, which cannot be compiled. Zod returns it uncompiled and it
|
|
74
|
+
* goes on parsing normally, so the option is safe to set on any spec.
|
|
75
|
+
*/
|
|
76
|
+
"compile-schemas"?: boolean;
|
|
45
77
|
/**
|
|
46
78
|
* Models whose PROPERTY NAMES are also emitted as a runtime tuple.
|
|
47
79
|
*
|
|
@@ -100,6 +132,7 @@ export interface EmitterOptions {
|
|
|
100
132
|
"contracts-output-dir"?: string;
|
|
101
133
|
"contracts-package"?: string;
|
|
102
134
|
"seal-object-schemas"?: boolean;
|
|
135
|
+
"compile-schemas"?: boolean;
|
|
103
136
|
"key-vocabularies"?: string[];
|
|
104
137
|
"runtime-module"?: string;
|
|
105
138
|
"regenerate-hint"?: string;
|
|
@@ -119,6 +152,9 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
119
152
|
"unsupported-default": {
|
|
120
153
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
121
154
|
};
|
|
155
|
+
"default-on-required-property": {
|
|
156
|
+
readonly default: import("@typespec/compiler").CallableMessage<["name", "name", "name"]>;
|
|
157
|
+
};
|
|
122
158
|
"undeclared-discriminator": {
|
|
123
159
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "property", "variant", "property", "variant"]>;
|
|
124
160
|
};
|
|
@@ -141,13 +177,16 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
141
177
|
readonly default: import("@typespec/compiler").CallableMessage<["name"]>;
|
|
142
178
|
};
|
|
143
179
|
}, EmitterOptions, never>;
|
|
144
|
-
export declare const reportDiagnostic: <C extends "duplicate-declaration" | "duplicate-operation-id" | "empty-union" | "undeclared-discriminator" | "unknown-key-vocabulary" | "unmirrorable-seal" | "unsupported-default" | "unsupported-status-code-range" | "unsupported-type", M extends keyof {
|
|
180
|
+
export declare const reportDiagnostic: <C extends "default-on-required-property" | "duplicate-declaration" | "duplicate-operation-id" | "empty-union" | "undeclared-discriminator" | "unknown-key-vocabulary" | "unmirrorable-seal" | "unsupported-default" | "unsupported-status-code-range" | "unsupported-type", M extends keyof {
|
|
145
181
|
"unsupported-type": {
|
|
146
182
|
readonly default: import("@typespec/compiler").CallableMessage<["artefact", "kind", "why", "remedy"]>;
|
|
147
183
|
};
|
|
148
184
|
"unsupported-default": {
|
|
149
185
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
150
186
|
};
|
|
187
|
+
"default-on-required-property": {
|
|
188
|
+
readonly default: import("@typespec/compiler").CallableMessage<["name", "name", "name"]>;
|
|
189
|
+
};
|
|
151
190
|
"undeclared-discriminator": {
|
|
152
191
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "property", "variant", "property", "variant"]>;
|
|
153
192
|
};
|
|
@@ -176,6 +215,9 @@ export declare const reportDiagnostic: <C extends "duplicate-declaration" | "dup
|
|
|
176
215
|
"unsupported-default": {
|
|
177
216
|
readonly default: import("@typespec/compiler").CallableMessage<["why"]>;
|
|
178
217
|
};
|
|
218
|
+
"default-on-required-property": {
|
|
219
|
+
readonly default: import("@typespec/compiler").CallableMessage<["name", "name", "name"]>;
|
|
220
|
+
};
|
|
179
221
|
"undeclared-discriminator": {
|
|
180
222
|
readonly default: import("@typespec/compiler").CallableMessage<["name", "property", "variant", "property", "variant"]>;
|
|
181
223
|
};
|
package/dist/src/lib.js
CHANGED
|
@@ -12,6 +12,7 @@ export const EmitterOptionsSchema = {
|
|
|
12
12
|
"contracts-output-dir": { type: "string", nullable: true },
|
|
13
13
|
"contracts-package": { type: "string", nullable: true },
|
|
14
14
|
"seal-object-schemas": { type: "boolean", nullable: true },
|
|
15
|
+
"compile-schemas": { type: "boolean", nullable: true },
|
|
15
16
|
"key-vocabularies": { type: "array", items: { type: "string" }, nullable: true },
|
|
16
17
|
"runtime-module": { type: "string", nullable: true },
|
|
17
18
|
"regenerate-hint": { type: "string", nullable: true },
|
|
@@ -26,6 +27,7 @@ export const EmitterOptionsSchema = {
|
|
|
26
27
|
"contracts-output-dir": { type: "string", nullable: true },
|
|
27
28
|
"contracts-package": { type: "string", nullable: true },
|
|
28
29
|
"seal-object-schemas": { type: "boolean", nullable: true },
|
|
30
|
+
"compile-schemas": { type: "boolean", nullable: true },
|
|
29
31
|
"key-vocabularies": { type: "array", items: { type: "string" }, nullable: true },
|
|
30
32
|
"runtime-module": { type: "string", nullable: true },
|
|
31
33
|
"regenerate-hint": { type: "string", nullable: true },
|
|
@@ -76,6 +78,42 @@ const diagnostics = {
|
|
|
76
78
|
default: paramMessage `Cannot emit a default value of ${"why"}. Scalars, arrays and objects are emitted as literals; a default built by a scalar constructor has no literal form. Replace it with a literal value, or drop the default and let the property be optional. The property keeps its declared shape and loses only the fallback.`,
|
|
77
79
|
},
|
|
78
80
|
},
|
|
81
|
+
/**
|
|
82
|
+
* **A default on a REQUIRED property, where the default can never apply.**
|
|
83
|
+
*
|
|
84
|
+
* **`default` is an annotation and `required` is the assertion**, and this emitter used to
|
|
85
|
+
* implement the opposite. JSON Schema 2020-12 section 9.2 makes `default` an annotation keyword
|
|
86
|
+
* with no effect on validation, and `@typespec/openapi3` implements exactly that:
|
|
87
|
+
* `#requiredModelProperties` builds the `required` list from `metadataInfo.isOptional` alone and
|
|
88
|
+
* never consults a default, then attaches the default beside it as a plain `schema.default`. The
|
|
89
|
+
* parameter side is the same rule spelled differently, `required: !param.optional`.
|
|
90
|
+
*
|
|
91
|
+
* So `basis: string = "s1159"` publishes as REQUIRED with an annotation nothing can reach. This
|
|
92
|
+
* emitter emitted `.default("s1159")`, which makes the field optional on the way IN - a validator
|
|
93
|
+
* accepting a request the document generated beside it forbids. OAI/OpenAPI-Specification#1543 is
|
|
94
|
+
* closed COMPLETED on this exact question, with the JSON Schema editor's answer that writing a
|
|
95
|
+
* default in before validating "is **NOT** part of validation", and the OpenAPI maintainer's that
|
|
96
|
+
* a default beside `required` "risks making the `required` notion invalid and encourages clients
|
|
97
|
+
* to unnecessarily send `default` values over the wire". Swagger's own documentation lists the
|
|
98
|
+
* combination as one of the two common mistakes with the keyword.
|
|
99
|
+
*
|
|
100
|
+
* **A WARNING, and this package's first, because the spec is representable.** `unsupported-default`
|
|
101
|
+
* used to be an error on composite literals and was retired for precisely this reason: refusing a
|
|
102
|
+
* construct `@typespec/openapi3` emits makes the same spec representable by one emitter and not the
|
|
103
|
+
* other, which is the one thing a differential between the two cannot tolerate. openapi3 emits this
|
|
104
|
+
* one, so this emitter serves it - saying what the document says, and saying out loud that the
|
|
105
|
+
* annotation is dead. The fix is one character and the message names it.
|
|
106
|
+
*
|
|
107
|
+
* The predicate is the DOCUMENT's, `metadataInfo.isOptional`, so this fires exactly where openapi3
|
|
108
|
+
* writes the name into `required` - including the PATCH position, where an update body's properties
|
|
109
|
+
* are optional and a default on one of them is live rather than dead.
|
|
110
|
+
*/
|
|
111
|
+
"default-on-required-property": {
|
|
112
|
+
severity: "warning",
|
|
113
|
+
messages: {
|
|
114
|
+
default: paramMessage `'${"name"}' is required, so its default can never apply: a required property is never absent. The OpenAPI document publishes it in 'required' and carries the default only as an annotation, and this validator does the same - a request omitting '${"name"}' is refused. Declare it optional, '${"name"}?: ... = ...', if callers may omit it.`,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
79
117
|
/**
|
|
80
118
|
* **A discriminated union whose discriminator the document never publishes.**
|
|
81
119
|
*
|
package/dist/src/types.js
CHANGED
|
@@ -13,9 +13,12 @@ import { discriminatedSubtypes, effectiveIndexer, inheritedAndOwnProperties, obj
|
|
|
13
13
|
*
|
|
14
14
|
* **It must agree with `zod.ts` exactly**, because the emitted assertion compares them. Every rule
|
|
15
15
|
* here has a counterpart there: `utcDateTime` is a string on both sides, `?` becomes
|
|
16
|
-
* `?: T | undefined` because `exactOptionalPropertyTypes` is on, and a
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* `?: T | undefined` because `exactOptionalPropertyTypes` is on, and a property the document leaves
|
|
17
|
+
* out of `required` is optional here whether its absence is spelled `?` or covered by a default.
|
|
18
|
+
*
|
|
19
|
+
* **The assertion pairs this walk against `z.input`, the type the validator ACCEPTS**, and not
|
|
20
|
+
* against `z.infer`, which is what it produces. These types are the floor a producer supplies, and
|
|
21
|
+
* for a request the producer is a caller who has not run the validator.
|
|
19
22
|
*/
|
|
20
23
|
/** Scalars we accept, and the TypeScript they become. Mirrors `SCALARS` in `zod.ts`. */
|
|
21
24
|
const SCALARS = {
|
|
@@ -247,15 +250,30 @@ export function propertyToTs(program, property, wireName) {
|
|
|
247
250
|
const encoded = getEncode(program, property)?.type;
|
|
248
251
|
const value = typeToTs(program, encoded ?? property.type);
|
|
249
252
|
/**
|
|
250
|
-
* **A defaulted property
|
|
253
|
+
* **A defaulted property used to be REQUIRED here, and that was the OUTPUT type's answer to a
|
|
254
|
+
* question this file does not ask.**
|
|
255
|
+
*
|
|
256
|
+
* The reasoning was: `zod.ts` emits `.default(v)`, the assertion compares against `z.infer` which
|
|
257
|
+
* is the output type, and by then the default has fired. Every step of that is true and the
|
|
258
|
+
* conclusion served the wrong reader. **These types are the floor a PRODUCER supplies**, and for
|
|
259
|
+
* a request the producer is the CALLER - who has not run the validator, and for whom the document
|
|
260
|
+
* is the only contract. The document leaves an optional defaulted property out of `required`, so
|
|
261
|
+
* a caller may omit it, and this file said they could not.
|
|
262
|
+
*
|
|
263
|
+
* Reported by a consumer building requests from a zero-dependency contracts package, carrying a
|
|
264
|
+
* named helper to relax six such properties by hand. The helper could only reach the top level; a
|
|
265
|
+
* default one level down stayed required and no caller could build the value at all. Emitting the
|
|
266
|
+
* optionality per property fixes every depth at once, because there is no depth to reach.
|
|
267
|
+
*
|
|
268
|
+
* **The two directions of the emitted set differ here on purpose**, exactly as they do on
|
|
269
|
+
* openness: `schemas.gen.ts` exports `Exact<z.infer<...>>`, the narrow view of what ARRIVES, in
|
|
270
|
+
* which a default has fired and the property is present. `wire-contract.gen.ts` pairs this walk
|
|
271
|
+
* against `z.input` for the same reason.
|
|
251
272
|
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* Marking it optional would make a correct schema fail its own assertion.
|
|
273
|
+
* A defaulted property that reaches here is one the document publishes as optional, because
|
|
274
|
+
* `propertyToZod` reports `default-on-required-property` for the other spelling and emits no
|
|
275
|
+
* `.default(...)` for it - so the two walks agree without either one restating the rule.
|
|
256
276
|
*/
|
|
257
|
-
if (property.defaultValue !== undefined)
|
|
258
|
-
return `${key}: ${value};`;
|
|
259
277
|
/**
|
|
260
278
|
* **`?: T | undefined`, and dropping the `| undefined` was tried in `0.19.0` and reverted.**
|
|
261
279
|
*
|
package/dist/src/zod.js
CHANGED
|
@@ -978,14 +978,6 @@ export function propertyToZod(program, property) {
|
|
|
978
978
|
const base = nullable === undefined || encoded !== undefined
|
|
979
979
|
? applyConstraints(program, typeToZod(program, declared), property)
|
|
980
980
|
: `${applyConstraints(program, typeToZod(program, declared), property)}.nullable()`;
|
|
981
|
-
/**
|
|
982
|
-
* A TypeSpec default (`basis: string = "s1159"`) becomes `.default(...)`, which in Zod also makes
|
|
983
|
-
* the field optional on the way in. Dropping it turned an optional field with a fallback into a
|
|
984
|
-
* required one - callers omitting it would start getting a 400 for a request that used to work.
|
|
985
|
-
*/
|
|
986
|
-
const defaulted = defaultOf(program, property);
|
|
987
|
-
if (defaulted !== undefined)
|
|
988
|
-
return `${base}.default(${defaulted})`;
|
|
989
981
|
/**
|
|
990
982
|
* **Optionality is a function of the position too, not only of the `?` in the spec.**
|
|
991
983
|
*
|
|
@@ -995,7 +987,59 @@ export function propertyToZod(program, property) {
|
|
|
995
987
|
* would reject every partial update the contract invites. `metadataInfo.isOptional` is the same
|
|
996
988
|
* predicate `@typespec/openapi3` uses to decide the `required` list.
|
|
997
989
|
*/
|
|
998
|
-
|
|
990
|
+
const optional = property.optional || isOptionalAt(program, property);
|
|
991
|
+
/**
|
|
992
|
+
* **A default only applies where the document says the property may be absent.**
|
|
993
|
+
*
|
|
994
|
+
* `.default(v)` makes a Zod field optional on the way IN, so emitting one for a property the
|
|
995
|
+
* document publishes as `required` made this validator accept a request the document beside it
|
|
996
|
+
* refuses. `default` is an ANNOTATION under JSON Schema 2020-12 and `required` is the assertion;
|
|
997
|
+
* `@typespec/openapi3` reads the two exactly that way, building `required` from
|
|
998
|
+
* `metadataInfo.isOptional` and never consulting a default. See `default-on-required-property`
|
|
999
|
+
* in `lib.ts` for the specification chain and for why this warns rather than refuses.
|
|
1000
|
+
*
|
|
1001
|
+
* Reported and then rendered as an ordinary required property - the declared shape is untouched
|
|
1002
|
+
* and only the dead fallback is dropped, which is the same shape `unsupported-default` already
|
|
1003
|
+
* takes. `defaultOf` is deliberately NOT called on this path: it would report a second diagnostic
|
|
1004
|
+
* about a value that cannot be reached either way, naming the less useful of the two problems.
|
|
1005
|
+
*/
|
|
1006
|
+
if (!optional && property.defaultValue !== undefined) {
|
|
1007
|
+
reportDiagnostic(program, {
|
|
1008
|
+
code: "default-on-required-property",
|
|
1009
|
+
target: property,
|
|
1010
|
+
format: { name: property.name },
|
|
1011
|
+
});
|
|
1012
|
+
return base;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* A TypeSpec default (`basis?: string = "s1159"`) becomes `.default(...)`, which in Zod also makes
|
|
1016
|
+
* the field optional on the way in - matching a document that leaves the property out of
|
|
1017
|
+
* `required` and annotates it with the value. `.optional()` is not added beside it: `.default()`
|
|
1018
|
+
* already accepts an absent value, and the two together would make the OUTPUT optional as well,
|
|
1019
|
+
* so the fallback could be skipped.
|
|
1020
|
+
*/
|
|
1021
|
+
const defaulted = defaultOf(program, property);
|
|
1022
|
+
if (defaulted !== undefined)
|
|
1023
|
+
return `${base}.default(${defaulted})`;
|
|
1024
|
+
/**
|
|
1025
|
+
* **`.exactOptional()`, because the document says the KEY may be absent and nothing more.**
|
|
1026
|
+
*
|
|
1027
|
+
* `.optional()` additionally accepts an explicit `undefined`, and **JSON cannot carry one** - so
|
|
1028
|
+
* for a body the validator was admitting a value no conformant request can contain, and for a
|
|
1029
|
+
* parameter one no transport produces. The document says exactly "absent or a value" by leaving
|
|
1030
|
+
* the property out of `required`, and this is the construct that says the same.
|
|
1031
|
+
*
|
|
1032
|
+
* **The emitted TYPE has said this since `0.17.0`; only the validator disagreed.** `Exact<>` was
|
|
1033
|
+
* written to strip `| undefined` back off `z.infer`, with a docblock arguing precisely this - so
|
|
1034
|
+
* one program published a type refusing `{ note: undefined }` beside a validator accepting it, and
|
|
1035
|
+
* nothing compared them. `z.infer` of `.exactOptional()` is `note?: T` natively and at every depth,
|
|
1036
|
+
* which a shallow mapped type never managed, so `Exact<>` is deleted rather than kept beside it.
|
|
1037
|
+
*
|
|
1038
|
+
* Measured on zod 4.5.2 before the change: `z.toJSONSchema` is byte-identical for the two, so the
|
|
1039
|
+
* differential's serialisation axis is untouched, and `def.type` is still `"optional"`, so the
|
|
1040
|
+
* shape describers read it unchanged.
|
|
1041
|
+
*/
|
|
1042
|
+
return optional ? `${base}.exactOptional()` : base;
|
|
999
1043
|
}
|
|
1000
1044
|
/**
|
|
1001
1045
|
* A default value as a JS literal, or `undefined` where this emitter cannot render one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typespec-http-zod",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "TypeSpec emitter and library: turn an HTTP service definition into Zod validators and framework-free contract types that agree, keyword for keyword, with the OpenAPI document @typespec/openapi3 publishes from the same source.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
@@ -55,13 +55,14 @@
|
|
|
55
55
|
"@typespec/streams": "0.85.0",
|
|
56
56
|
"@typespec/versioning": "0.85.0",
|
|
57
57
|
"@typespec/xml": "0.85.0",
|
|
58
|
+
"ajv": "^8.20.0",
|
|
58
59
|
"oxfmt": "^0.63.0",
|
|
59
60
|
"oxlint": "^1.78.0",
|
|
60
61
|
"typescript": "~7.0.2",
|
|
61
62
|
"typespec-http-zod": "link:.",
|
|
62
63
|
"vitest": "^4.1.9",
|
|
63
64
|
"yaml": "^2.9.0",
|
|
64
|
-
"zod": "^4.
|
|
65
|
+
"zod": "^4.5.2"
|
|
65
66
|
},
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"@typespec/compiler": "^1.15.0",
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
"@typespec/openapi": "^1.15.0",
|
|
70
71
|
"@typespec/streams": "^0.85.0",
|
|
71
72
|
"@typespec/versioning": "^0.85.0",
|
|
72
|
-
"zod": "^4.
|
|
73
|
+
"zod": "^4.5.0"
|
|
73
74
|
},
|
|
74
75
|
"peerDependenciesMeta": {
|
|
75
76
|
"@typespec/streams": {
|