typespec-http-zod 0.29.0 → 0.31.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/dist/src/zod.js CHANGED
@@ -1,7 +1,11 @@
1
- import { getDiscriminatedUnion, getDiscriminator, getDoc, getEncode, resolveEncodedName, isNeverType, isTemplateDeclaration, } from "@typespec/compiler";
2
- import { createMetadataInfo, getCookieParamOptions, getHeaderFieldName, getPathParamName, getQueryParamName, isCookieParam, isHeader, isPathParam, isQueryParam, Visibility, } from "@typespec/http";
3
- import { applyConstraints } from "./constraints.js";
1
+ import { getDiscriminatedUnion, getDiscriminatedUnionFromInheritance, getDiscriminator, getDoc, getEncode, getTypeName, ignoreDiagnostics, resolveEncodedName, serializeValueAsJson, isNeverType, isTemplateDeclaration, } from "@typespec/compiler";
2
+ import { createMetadataInfo, getCookieParamOptions, getHeaderFieldName, getVisibilitySuffix, getPathParamName, getQueryParamName, isCookieParam, isHeader, isPathParam, isQueryParam, Visibility, } from "@typespec/http";
3
+ import { $ } from "@typespec/compiler/typekit";
4
+ import { z } from "zod";
5
+ import { isReadonlyProperty, shouldInline } from "@typespec/openapi";
6
+ import { applyConstraints, firstReportOf, brokenConstraintOf, brokenItemsOf, hasConstraints, } from "./constraints.js";
4
7
  import { literal } from "./literal.js";
8
+ import { isPublishedSplit, isSharedRouteOptional, isSuffixedByUsage, keptVisibilityOf, readVisibilityOf, } from "./published.js";
5
9
  import { reportDiagnostic } from "./lib.js";
6
10
  /**
7
11
  * What a walk returns after reporting a refusal.
@@ -22,14 +26,19 @@ export const UNREPRESENTABLE = "z.never()";
22
26
  * in rather than computed here.
23
27
  */
24
28
  let annotateDeferred = () => undefined;
25
- export function withDeferredAnnotator(annotator, run) {
29
+ /** The same, for a value no property declares: an envelope's `value`, typed as its variant. */
30
+ let annotateDeferredType = () => undefined;
31
+ export function withDeferredAnnotator(annotator, typeAnnotator, run) {
26
32
  const previous = annotateDeferred;
33
+ const previousType = annotateDeferredType;
27
34
  annotateDeferred = annotator;
35
+ annotateDeferredType = typeAnnotator;
28
36
  try {
29
37
  return run();
30
38
  }
31
39
  finally {
32
40
  annotateDeferred = previous;
41
+ annotateDeferredType = previousType;
33
42
  }
34
43
  }
35
44
  /**
@@ -127,52 +136,255 @@ export function withSealedObjects(sealed, run) {
127
136
  * from answering differently. Measured on `type/model/visibility`: openapi3 emits five components
128
137
  * for one model, and before this we emitted one carrying every lifecycle property at once.
129
138
  *
130
- * **Position is a SECOND axis, and this only handles the first.** `@header` is metadata at a
131
- * payload root and ordinary data nested inside a `@body`; `isPayloadProperty` takes a third
132
- * argument, `ignoreMetadataAnnotations`, which openapi3 threads as emitter context
133
- * (`schema-emitter.js:210`) and this does not pass. A metadata-carrying model used in **both**
134
- * positions therefore still loses the header property where the document keeps it.
135
- *
136
- * That shape is rare rather than theoretical: it raises `metadata-ignored` from `@typespec/http`,
137
- * which fires **zero** times across all 65 `@typespec/http-specs` scenarios,
138
- * and one variant of it makes openapi3 itself fail with `duplicate-type-name`. It is a real gap with
139
- * no known occurrence - recorded here rather than described as an incident, because it has not been
140
- * one.
139
+ * **Position is a SECOND axis**: `@header` is metadata at a payload root and ordinary data inside
140
+ * an explicit `@body` - see {@link withMetadataAnnotationsIgnored}, which carries openapi3's third
141
+ * argument to `isPayloadProperty`.
141
142
  *
142
143
  * Scoped rather than threaded through six signatures, matching the idiom the rest of this module
143
144
  * already uses - the walk is deeply recursive and a parameter would have to be carried by every
144
145
  * function whether it cared or not.
145
146
  */
146
147
  let visibility = Visibility.Read;
147
- let metadataInfo;
148
- export function withVisibility(program, next, run) {
148
+ /**
149
+ * **One metadata view per program**, because the compiler's state lives on the program. Kept in one
150
+ * module variable, a view built for one compile was read by the next one in the same process -
151
+ * whatever first asked outside a walk left it there - and that program's `@visibility` and `@header`
152
+ * were invisible to it: a create-only or `@header` declaration counted as payload in every compile
153
+ * after the first.
154
+ */
155
+ const metadataInfos = new WeakMap();
156
+ /**
157
+ * **openapi3's own metadata view: `Read` is canonical, and a read-only property is shareable.**
158
+ *
159
+ * Built with `@typespec/http`'s defaults, the canonical visibility was `All`, so a model whose
160
+ * create-only property is visible at `All` was not transformed at `Create`: its create body
161
+ * collapsed onto the bare declaration, and every response was then validated as the CREATE view -
162
+ * `{name}` refused for want of the `secret` no response carries. And a read-only `id` made a model
163
+ * transformed at `Create`, where openapi3 shares the canonical schema, so the create body was
164
+ * validated without the `id` the document requires. Both are openapi3's arguments in
165
+ * `openapi.js`, passed as it passes them.
166
+ */
167
+ export function metadataInfoOf(program) {
168
+ let info = metadataInfos.get(program);
169
+ if (info === undefined) {
170
+ info = createMetadataInfo(program, {
171
+ canonicalVisibility: Visibility.Read,
172
+ canShareProperty: (property) => isReadonlyProperty(program, property),
173
+ });
174
+ metadataInfos.set(program, info);
175
+ }
176
+ return info;
177
+ }
178
+ export function withVisibility(next, run) {
149
179
  const previousVisibility = visibility;
150
- const previousInfo = metadataInfo;
151
180
  visibility = next;
152
- metadataInfo ??= createMetadataInfo(program);
153
181
  try {
154
182
  return run();
155
183
  }
156
184
  finally {
157
185
  visibility = previousVisibility;
158
- metadataInfo = previousInfo;
159
186
  }
160
187
  }
161
188
  /** Whether a property is optional AT THE CURRENT POSITION - PATCH makes update fields optional. */
162
189
  export function isOptionalAt(program, property) {
163
- metadataInfo ??= createMetadataInfo(program);
164
- return metadataInfo.isOptional(property, visibility);
190
+ return (metadataInfoOf(program).isOptional(property, visibility) || isSharedRouteOptional(property));
165
191
  }
166
192
  /** Whether a property belongs in the BODY at the current position, rather than being metadata. */
167
193
  export function isPayloadProperty(program, property) {
168
- metadataInfo ??= createMetadataInfo(program);
169
- return metadataInfo.isPayloadProperty(property, visibility);
194
+ return metadataInfoOf(program).isPayloadProperty(property, visibility, metadataIgnored);
195
+ }
196
+ /**
197
+ * **Whether the walk is inside an explicit `@body` whose type carries metadata annotations.**
198
+ *
199
+ * `@header` is metadata at a payload root and ordinary data inside a `@body`: `@typespec/http`
200
+ * ignores the annotation there, and openapi3 renders such a body - and every type under it - with
201
+ * `ignoreMetadataAnnotations`, so `@header("x-code") @maxLength(3) code` is published as a body
202
+ * property with its constraint. Not passed, the property was dropped from the body the document
203
+ * requires it in; and a model's `@header` read as data everywhere enforced its `@maxLength` on a
204
+ * response that never carries it. Both are openapi3's one rule: `body.isExplicit &&
205
+ * body.containsMetadataAnnotations`.
206
+ */
207
+ let metadataIgnored = false;
208
+ export function withMetadataAnnotationsIgnored(ignored, run) {
209
+ const previous = metadataIgnored;
210
+ metadataIgnored = ignored;
211
+ try {
212
+ return run();
213
+ }
214
+ finally {
215
+ metadataIgnored = previous;
216
+ }
217
+ }
218
+ /**
219
+ * **Whether the walk is inside a multipart part whose union holds raw `bytes`**: openapi3 renders
220
+ * such a part in the form's own content type, where a `bytes` member of a union is the raw binary
221
+ * `{}` - any value - rather than a base64 string. `HttpPart<bytes | string>` is `anyOf: [{}, string]`,
222
+ * and validated with `bytes` as a string it refused every file the document accepts. Keyed on by the
223
+ * registries, as a named union published there is a schema of its own.
224
+ */
225
+ let multipartBinary = false;
226
+ export function withMultipartBinary(binary, run) {
227
+ const previous = multipartBinary;
228
+ multipartBinary = binary;
229
+ try {
230
+ return run();
231
+ }
232
+ finally {
233
+ multipartBinary = previous;
234
+ }
235
+ }
236
+ export function multipartBinaryContext() {
237
+ return multipartBinary;
238
+ }
239
+ /** `bytes` kept raw, as openapi3's `isBytesKeptRaw` reads it: the standard scalar, not encoded. */
240
+ export function isRawBytes(program, type) {
241
+ return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined;
242
+ }
243
+ /** A union one of whose own members is raw `bytes` - see `withMultipartBinary`. */
244
+ export function holdsRawBytes(program, type) {
245
+ return (type.kind === "Union" &&
246
+ [...type.variants.values()].some((variant) => isRawBytes(program, variant.type)));
247
+ }
248
+ /** Whether metadata annotations are ignored where the walk is - the registry keys declarations on it. */
249
+ export function metadataAnnotationsIgnored() {
250
+ return metadataIgnored;
251
+ }
252
+ /** Whether this model or union is shaped differently at `visibility` than at the canonical `Read`. */
253
+ export function isTransformedBy(program, type, at) {
254
+ return metadataInfoOf(program).isTransformed(type, at);
255
+ }
256
+ /**
257
+ * **The visibility a type is rendered at where another type refers to it: openapi3's
258
+ * `reduceContext`.** A type that is not transformed at the requested visibility renders at `Read`, so
259
+ * a model with nothing create-specific inside a create body IS the canonical schema - a read-only
260
+ * property shared into it, as `canShareProperty` says. Rendered at the requested visibility under the
261
+ * canonical name, the body dropped that property while the document requires it.
262
+ *
263
+ * **Only where openapi3 reduces.** Its asset emitter asks for a reference context by what it emits:
264
+ * a named model, a model expression, a template instance, a named union, a scalar or an enum. A list -
265
+ * `T[]` or `model Tags is Tag[]` - and a union expression or instance have none, so they keep the
266
+ * visibility they were reached at, and so does everything inside them. Reduced here as well, a
267
+ * `model Tags is Tag[]` sent in a create body and returned was one declaration where the document
268
+ * publishes `TagsCreate` and `Tags`, and a model expression inside a union expression in a create
269
+ * body was judged at `Read`, where openapi3 reads its properties at `Create`.
270
+ */
271
+ export function renderedVisibilityOf(program, type, requested) {
272
+ if (requested === Visibility.Read)
273
+ return Visibility.Read;
274
+ switch (type.kind) {
275
+ // A reference to a variant, `U.a`, is its type's - see `publicationOf`.
276
+ case "UnionVariant":
277
+ return renderedVisibilityOf(program, type.type, requested);
278
+ case "Model":
279
+ if (isArrayLiteral(program, type) || isNamedList(type))
280
+ return requested;
281
+ return isTransformedBy(program, type, requested) ? requested : Visibility.Read;
282
+ case "Union":
283
+ if (type.name === undefined || type.templateMapper !== undefined)
284
+ return requested;
285
+ return isTransformedBy(program, type, requested) ? requested : Visibility.Read;
286
+ // A tuple is written out where it is used, each element at the visibility around it.
287
+ case "Tuple":
288
+ return requested;
289
+ default:
290
+ return Visibility.Read;
291
+ }
292
+ }
293
+ /**
294
+ * **The visibility an operation's body, parameter or response is rendered at: openapi3's
295
+ * `emitTypeWithSchemaEmitter`**, which renders a type at `Read` unless it is transformed at the
296
+ * requested visibility - whatever kind of type it is, a list included.
297
+ */
298
+ export function rootVisibilityOf(program, type, requested) {
299
+ if (type.kind === "UnionVariant")
300
+ return rootVisibilityOf(program, type.type, requested);
301
+ return requested !== Visibility.Read &&
302
+ (type.kind === "Model" || type.kind === "Union") &&
303
+ isTransformedBy(program, type, requested)
304
+ ? requested
305
+ : Visibility.Read;
306
+ }
307
+ /**
308
+ * **Whether openapi3 can name a template instance**: its asset emitter's `declarationName`, which
309
+ * spells each argument's own name after the template's and gives up where one has none - a model
310
+ * expression, a literal, an instance of no nameable arguments. Such an instance is written out where
311
+ * it is used: `Maybe<{ x: string }>` is no component, and declared as `Maybe` it was one name for two
312
+ * schemas wherever a second argument appeared.
313
+ */
314
+ export function isSpeakableInstance(type) {
315
+ if (!("templateMapper" in type) || type.templateMapper === undefined)
316
+ return true;
317
+ return type.templateMapper.args.every((argument) => {
318
+ const each = "entityKind" in argument && argument.entityKind === "Indeterminate"
319
+ ? argument.type
320
+ : argument;
321
+ if (!("kind" in each))
322
+ return false;
323
+ switch (each.kind) {
324
+ case "Model":
325
+ case "Scalar":
326
+ case "Interface":
327
+ case "Operation":
328
+ case "Enum":
329
+ case "Union":
330
+ case "Intrinsic":
331
+ return typeof each.name === "string" && each.name !== "" && isSpeakableInstance(each);
332
+ default:
333
+ return false;
334
+ }
335
+ });
336
+ }
337
+ /** A named list model, `model Tags is Tag[]`, which openapi3 emits as an `arrayDeclaration`. */
338
+ export function isNamedList(type) {
339
+ return (type.name !== "" && type.templateMapper === undefined && type.indexer?.key.name === "integer");
340
+ }
341
+ /**
342
+ * **An array literal's elements are rendered at `Item` visibility**, as openapi3's
343
+ * `arrayLiteralReferenceContext` renders them: PATCH leaves a list's items required, and metadata on
344
+ * an element is data, since no element carries a header. Read at the list's own visibility, a PATCH
345
+ * made every item's properties optional, a `@header` on an element dropped out of the list the
346
+ * document requires it in, and one such model in a request list and a response list was two shapes
347
+ * under one name - `TaggedCreateItem` and `TaggedItem` in the document. A named list model is a
348
+ * declaration, rendered at its own visibility.
349
+ */
350
+ export function atItem(program, list, run) {
351
+ return isArrayLiteral(program, list) ? withVisibility(visibility | Visibility.Item, run) : run();
352
+ }
353
+ /**
354
+ * **An array literal, `T[]`, rather than a named list model** - as openapi3's asset emitter tells
355
+ * them apart, by name: a list called `Array` with no properties of its own. A visibility template's
356
+ * copy of `Pet[]` is no standard type, and read as a named list it was declared as `Array`, one
357
+ * name for every such copy.
358
+ */
359
+ export function isArrayLiteral(program, list) {
360
+ return $(program).array.is(list) && list.name === "Array";
361
+ }
362
+ /**
363
+ * **Whether the walk is rendering an operation's parameter or response header itself**, outside any
364
+ * declaration it refers to. A tuple there is refused: openapi3 publishes `prefixItems` over the
365
+ * values the parameter carries, and nothing here reads a parameter's text as positions. A model a
366
+ * parameter refers to is a component the document shares with every other use, and a tuple in it is
367
+ * validated as the document publishes it.
368
+ */
369
+ let parameterPosition = false;
370
+ export function withParameterPosition(inside, run) {
371
+ const previous = parameterPosition;
372
+ parameterPosition = inside;
373
+ try {
374
+ return run();
375
+ }
376
+ finally {
377
+ parameterPosition = previous;
378
+ }
170
379
  }
171
- /** Whether this model is shaped differently at `visibility` than at the canonical `Read`. */
172
- export function isTransformedBy(program, model, at) {
173
- metadataInfo ??= createMetadataInfo(program);
174
- return metadataInfo.isTransformed(model, at);
380
+ export function inParameterPosition() {
381
+ return parameterPosition;
175
382
  }
383
+ /** Why a tuple parameter is refused, and what to write instead - both walks say the same. */
384
+ export const TUPLE_PARAMETER = {
385
+ why: "a parameter typed as a tuple, which openapi3 publishes as `prefixItems` over the values it carries and this emitter does not read from the wire",
386
+ remedy: "Declare the parameter as a list of one element type, or carry the value in the body.",
387
+ };
176
388
  /** The visibility the current walk is emitting for - the registry keys declarations on it. */
177
389
  export function currentVisibility() {
178
390
  return visibility;
@@ -183,7 +395,7 @@ export function currentVisibility() {
183
395
  * A template declaration is not a real subtype, and neither is an uninstantiated template; both
184
396
  * would otherwise stop a base from being sealed for no reason a reader could see.
185
397
  */
186
- function includeDerivedModel(model) {
398
+ export function includeDerivedModel(model) {
187
399
  return (!isTemplateDeclaration(model) &&
188
400
  (model.templateMapper?.args === undefined ||
189
401
  model.templateMapper.args.length === 0 ||
@@ -233,7 +445,7 @@ export function noteKeyVocabularies(program, models, wanted) {
233
445
  if (!wanted.includes(model.name))
234
446
  continue;
235
447
  found.push(model.name);
236
- noteVocabulary(model.name, inheritedAndOwnProperties(model).map((property) => rawPropertyKey(program, property)));
448
+ noteVocabulary(model.name, inheritedAndOwnProperties(program, model).map((property) => rawPropertyKey(program, property)));
237
449
  }
238
450
  return found;
239
451
  }
@@ -326,15 +538,76 @@ export function withDeclarationSite(site, run) {
326
538
  export function refusalTarget(type) {
327
539
  return type.node === undefined ? (declarationSite ?? type) : type;
328
540
  }
541
+ /**
542
+ * **The external document a type is published as a reference to**, by `@useRef`: openapi3 writes
543
+ * `$ref: <url>` wherever it is used and no component, so the document states no schema this emitter
544
+ * can read. Read off the decorator applied, as `@typespec/openapi3`, which declares it, is no
545
+ * dependency of this package.
546
+ */
547
+ export function externalReferenceOf(type) {
548
+ if (!("decorators" in type))
549
+ return undefined;
550
+ const applied = type.decorators.find((each) => each.decorator.name === "$useRef");
551
+ if (applied === undefined)
552
+ return undefined;
553
+ const [argument] = applied.args;
554
+ return typeof argument?.jsValue === "string" ? argument.jsValue : "an external document";
555
+ }
556
+ /** Refuse a type the document publishes only as a reference to another document. */
557
+ export function refuseExternalReference(program, type, url) {
558
+ return refuse(program, type, `it is published as a reference to ${url}, whose schema the document does not state`, "Remove '@useRef' to have the document and this validator state the model, or validate the value yourself where it is used.");
559
+ }
329
560
  /** Refuse `type`, pointing at its declaration, and carry on so the rest is reported too. */
330
561
  function refuse(program, type, why, remedy = "") {
331
- reportDiagnostic(program, {
332
- code: "unsupported-type",
333
- target: refusalTarget(type),
334
- format: { artefact: "Zod", kind: type.kind, why, remedy },
335
- });
562
+ // Once per declaration, whichever walk reaches it first: both walk every type.
563
+ const target = refusalTarget(type);
564
+ if (firstReportOf(program, "unsupported-type", target, `${type.kind} ${why}`)) {
565
+ reportDiagnostic(program, {
566
+ code: "unsupported-type",
567
+ target,
568
+ format: { artefact: "Zod", kind: type.kind, why, remedy },
569
+ });
570
+ }
336
571
  return UNREPRESENTABLE;
337
572
  }
573
+ /**
574
+ * Whether a JSON value passes the check a standard scalar emits, asked of that very check: the
575
+ * table below is code this module writes, so it is compiled once here with the same Zod and run,
576
+ * rather than restated as a second set of rules that could drift from it. `undefined` where the
577
+ * scalar has no standard base or no entry.
578
+ */
579
+ const scalarChecks = new Map();
580
+ function scalarAccepts(program, scalar, json) {
581
+ /**
582
+ * **A scalar `@encode`d where it is declared is checked as the scalar it is encoded as**, as
583
+ * `scalarSchemaOf` validates it: `@encode(unixTimestamp, int32) scalar Ts` holds `1577836800`, and
584
+ * asked of `utcDateTime`'s ISO check the default `Ts.fromISO(...)` publishes was refused.
585
+ */
586
+ const encoding = scalarEncodingOf(program, scalar);
587
+ if (encoding !== undefined) {
588
+ return program.checker.isStdType(encoding.encoded)
589
+ ? scalarAccepts(program, encoding.encoded, json)
590
+ : undefined;
591
+ }
592
+ const base = standardBaseOf(program, scalar);
593
+ const source = base === undefined ? undefined : SCALARS[base.name];
594
+ if (base === undefined || source === undefined)
595
+ return undefined;
596
+ let check = scalarChecks.get(base.name);
597
+ if (check === undefined) {
598
+ check = new Function("z", `return ${source};`)(z);
599
+ scalarChecks.set(base.name, check);
600
+ }
601
+ return check.safeParse(json).success;
602
+ }
603
+ /**
604
+ * **The Zod a standard scalar becomes, or `undefined` for any other scalar** - looked up only for
605
+ * the standard library's own: a spec's `scalar uint8 extends string` is a string, and found in this
606
+ * table by its name it was validated as a byte, refusing every string the document accepts.
607
+ */
608
+ export function standardScalarSchemaOf(program, scalar) {
609
+ return program.checker.isStdType(scalar) ? SCALARS[scalar.name] : undefined;
610
+ }
338
611
  /** Scalars we accept, and the Zod they become. Keyed by the TypeSpec scalar's declared name. */
339
612
  const SCALARS = {
340
613
  string: "z.string()",
@@ -452,18 +725,60 @@ export function scalarDocOf(program, scalar) {
452
725
  }
453
726
  return undefined;
454
727
  }
728
+ /**
729
+ * **The `@encode` a scalar is published under: its own, or the nearest one up its `extends` chain.**
730
+ *
731
+ * openapi3 builds a scalar that is not standard from its base's schema - encoding included - and
732
+ * then applies its own decorators over it, so `scalar TsChild extends Ts` with
733
+ * `@encode(unixTimestamp, int32)` on `Ts` is published as an integer, exactly as `Ts` is. This read
734
+ * the scalar's own `@encode` alone, so `TsChild` was validated as an ISO date-time string, refusing
735
+ * every value the document accepts and accepting every value it refuses. `link` is the scalar that
736
+ * declares the encoding.
737
+ */
738
+ export function scalarEncodingOf(program, scalar) {
739
+ for (let link = scalar; link !== undefined; link = link.baseScalar) {
740
+ const encoded = encodedTypeOf(program, link);
741
+ if (encoded !== undefined)
742
+ return { link, encoded };
743
+ }
744
+ return undefined;
745
+ }
455
746
  /** A scalar's schema without its description, for a position that publishes its own. */
456
747
  function scalarSchemaOf(program, scalar) {
457
748
  // `scalar unixTimestamp32 is utcDateTime;` with `@encode(int32)` on the SCALAR, not on each use.
458
- const encoded = encodedTypeOf(program, scalar);
459
- if (encoded !== undefined && encoded !== scalar) {
749
+ const encoding = scalarEncodingOf(program, scalar);
750
+ if (encoding !== undefined) {
751
+ const { link, encoded } = encoding;
752
+ /**
753
+ * **An encoding to a scalar that is not standard publishes no type**, so there is no wire form
754
+ * to validate - including a scalar encoded as itself, which openapi3 publishes as
755
+ * `{ format: "y" }` alone. A scalar from the standard library carries no `@encode` of its own,
756
+ * so refusing this at the first link is also what ends every chain: `Ping -> Pong -> Ping` is
757
+ * refused at `Ping`, where it used to be followed until the stack overflowed.
758
+ */
759
+ if (!program.checker.isStdType(encoded)) {
760
+ reportUnsatisfiableEncoding(program, link, {
761
+ name: link.name,
762
+ declared: link.baseScalar?.name ?? link.name,
763
+ encoded: encoded.name,
764
+ why: "typeless",
765
+ });
766
+ return UNREPRESENTABLE;
767
+ }
768
+ /**
769
+ * The constraints are read from the scalar named, up its chain, and each one the encoded type
770
+ * cannot carry is dropped and reported by `applyConstraints`: openapi3 applies them BEFORE the
771
+ * encoding replaces `type`, so a `@pattern` on a string encoded as `int32` is published beside
772
+ * `type: integer`, where JSON Schema ignores it. Applied, it emitted `z.int32().regex(...)`,
773
+ * and the module threw on import.
774
+ */
460
775
  return applyConstraints(program, scalarSchemaOf(program, encoded), scalar);
461
776
  }
462
777
  let current = scalar;
463
778
  // Walk the `extends` chain so a named scalar (`scalar chNumber extends string`) resolves to the
464
779
  // primitive it is built on rather than being rejected for having a project-specific name.
465
780
  while (current !== undefined) {
466
- const mapped = SCALARS[current.name];
781
+ const mapped = standardScalarSchemaOf(program, current);
467
782
  // Constraints are read from the scalar the property NAMED, not from the primitive it resolves
468
783
  // to, so `@pattern(...) scalar TrimmedString extends string` carries its rules to every use.
469
784
  if (mapped !== undefined)
@@ -556,12 +871,55 @@ export function withSyntheticDeclarations(hook, run) {
556
871
  }
557
872
  }
558
873
  const capitalise = (value) => value.charAt(0).toUpperCase() + value.slice(1);
559
- /** Whether a variant already carries the discriminator, under its declared or its wire name. */
560
- function declaresDiscriminator(program, variant, property) {
874
+ /**
875
+ * **Why a variant of an envelope-less `@discriminated` union cannot be dispatched to by its wire
876
+ * discriminator, or `undefined` where it can.** The variant must carry the discriminator under its
877
+ * WIRE name, as one fixed value. Matched on the TypeSpec name as well, a variant whose `kind` was
878
+ * renamed `t` passed, and the module threw "Invalid discriminated union option" on import; one
879
+ * inheriting an open `type: string` threw on every parse.
880
+ */
881
+ export function undeclaredVariantOf(program, variant, property) {
561
882
  if (variant.kind !== "Model")
562
- return false;
563
- return inheritedAndOwnProperties(variant).some((candidate) => candidate.name === property ||
564
- resolveEncodedName(program, candidate, JSON_MEDIA_TYPE) === property);
883
+ return "missing";
884
+ const entry = publishedPropertiesOf(program, variant).find((candidate) => rawPropertyKey(program, candidate.property) === property);
885
+ if (entry === undefined)
886
+ return "missing";
887
+ if (fixedValuesOf(entry.property.type) === undefined)
888
+ return "open";
889
+ // A value a declaration above the variant refuses: no value is one.
890
+ return positionOf(program, variant, entry).never ? "refused" : undefined;
891
+ }
892
+ /**
893
+ * **The variants a `@discriminated` union dispatches to: each but one no value can be.**
894
+ *
895
+ * A variant without an envelope that accepts nothing - its fixed discriminator refused by a
896
+ * declaration above it, `kind: "cobra"` under `@maxLength(3) kind: string`, or a key it requires
897
+ * refused by a base openapi3 seals - is published by the document as a member no value satisfies, and
898
+ * a `oneOf` with such a member accepts what the others accept. Refusing the union for it refused every
899
+ * `asp` the document accepts, and dispatching to it put a `z.never()` where Zod reads a literal: the
900
+ * first parse threw "Invalid discriminated union option".
901
+ */
902
+ export function dispatchedVariantsOf(program, union) {
903
+ const [discriminated] = getDiscriminatedUnion(program, union);
904
+ if (discriminated === undefined)
905
+ return [];
906
+ const { envelope, discriminatorPropertyName } = discriminated.options;
907
+ return [...discriminated.variants.entries()]
908
+ .map(([name, variant]) => [String(name), variant])
909
+ .filter(([, variant]) => envelope !== "none" ||
910
+ /**
911
+ * A refused discriminator leaves values where it may be left out: under a PATCH,
912
+ * `NCUpdate` accepts `{}`, which matches `NAUpdate` too, so the document's `DNUpdate`
913
+ * refuses it. Dropped, `DNUpdate` was `NAUpdate` and accepted it.
914
+ */
915
+ ((undeclaredVariantOf(program, variant, discriminatorPropertyName) !== "refused" ||
916
+ optionalDiscriminator(program, union, variant, discriminated.options)) &&
917
+ !(variant.kind === "Model" && acceptsNothing(program, variant)) &&
918
+ !isPublishedSplit(program, variant, renderedVisibilityOf(program, variant, visibility))));
919
+ }
920
+ /** A model no value is, refused on its own - `invalid-subtype-discriminator`, `sealed-base-extended`. */
921
+ function acceptsNothing(program, model) {
922
+ return (unsatisfiableSubtype(program, model) || sealedBaseExtensionOf(program, model)?.required === true);
565
923
  }
566
924
  /**
567
925
  * One variant of a `@discriminated` union, as it appears ON THE WIRE.
@@ -580,18 +938,84 @@ function declaresDiscriminator(program, variant, property) {
580
938
  * emitted **only when the variant declares that property itself**. Otherwise the document does not
581
939
  * publish it and the union is refused; see `undeclared-discriminator`.
582
940
  */
941
+ /**
942
+ * **Whether a required property is optional at `at`**: a PATCH with implicit optionality, outside a
943
+ * list's elements - `@typespec/http`'s `isOptional`, for a property with no declaration of its own
944
+ * to ask, as an envelope's are.
945
+ */
946
+ export function implicitlyOptional(at) {
947
+ return ((at & Visibility.Update) !== 0 && (at & Visibility.Patch) !== 0 && (at & Visibility.Item) === 0);
948
+ }
949
+ /**
950
+ * **The visibility an envelope is published at, within a union rendered at the current one.**
951
+ * openapi3 synthesises the envelope as a model of two properties - the discriminator and the variant
952
+ * - and emits it from the union's context, which reduces it to `Read` unless it is transformed
953
+ * there: where the variant is, or where a PATCH makes both its properties optional. Read as one
954
+ * envelope for every visibility, a PATCH body was held to the read wrapper's required `kind` and
955
+ * `value`, and a response to the create wrapper's variant.
956
+ */
957
+ export function envelopeVisibilityOf(program, union, variant) {
958
+ /**
959
+ * **The union is reduced first**, as openapi3 reduces any reference to it: a union not
960
+ * transformed where it is reached is emitted at `Read`, its envelopes with it. Spelled out inside a
961
+ * merge under a PATCH, `DU { a: A, b: B }` of all-optional variants had its envelopes' `kind` and
962
+ * `value` made optional, where the document's `DUA` requires both.
963
+ */
964
+ const at = renderedVisibilityOf(program, union, visibility);
965
+ if (at === Visibility.Read)
966
+ return Visibility.Read;
967
+ const reshaped = (variant.kind === "Model" || variant.kind === "Union") && isTransformedBy(program, variant, at);
968
+ return reshaped || implicitlyOptional(at) ? at : Visibility.Read;
969
+ }
970
+ /** Whether a variant's discriminator may be absent where the union is rendered - see `unionToZod`. */
971
+ export function optionalDiscriminator(program, union, variant, options) {
972
+ if (options.envelope !== "none")
973
+ return implicitlyOptional(envelopeVisibilityOf(program, union, variant));
974
+ if (variant.kind !== "Model")
975
+ return false;
976
+ const at = renderedVisibilityOf(program, variant, visibility);
977
+ const property = inheritedAndOwnProperties(program, variant).find((each) => each.name === options.discriminatorPropertyName);
978
+ return property !== undefined && (property.optional || implicitlyOptional(at));
979
+ }
583
980
  function discriminatedVariant(program, union, variantName, variant, options) {
584
- const inner = typeToZod(program, variant);
585
- const tag = `${objectKey(options.discriminatorPropertyName)}: z.literal(${literal(variantName)})`;
981
+ const { value: inner, deferred } = captureBackEdges(() => typeToZod(program, variant));
982
+ const envelopeAt = envelopeVisibilityOf(program, union, variant);
983
+ const optional = implicitlyOptional(envelopeAt) ? ".exactOptional()" : "";
984
+ const tag = `${objectKey(options.discriminatorPropertyName)}: z.literal(${literal(variantName)})${optional}`;
586
985
  // The variant already carries the discriminator, so the document publishes it and there is
587
986
  // nothing to add.
588
- if (options.envelope === "none")
987
+ if (options.envelope === "none") {
988
+ if (deferred)
989
+ noteBackEdge();
589
990
  return inner;
590
- const wrapper = `${sealObjectSchemas ? "z.strictObject" : "z.object"}({ ${tag}, ${objectKey(options.envelopePropertyName)}: ${inner} })`;
591
- const name = `${union.name ?? ""}${capitalise(variantName)}`;
592
- if (union.name === undefined)
991
+ }
992
+ /**
993
+ * **A variant still being declared is reached through a getter**, as an object's member is: the
994
+ * wrapper is a declaration of its own, and read eagerly it met `catSchema` before its `const` -
995
+ * "Cannot access 'catSchema' before initialization" on import, for a `Cat` that refers back to the
996
+ * union through its own properties.
997
+ */
998
+ const key = objectKey(options.envelopePropertyName);
999
+ const value = deferred
1000
+ ? `get ${key}()${optional === "" ? (annotateDeferredType(variant) ?? "") : ""} { return ${inner}${optional}; }`
1001
+ : `${key}: ${inner}${optional}`;
1002
+ const wrapper = `${sealObjectSchemas ? "z.strictObject" : "z.object"}({ ${tag}, ${value} })`;
1003
+ const name = envelopeNameOf(program, union, variantName, variant);
1004
+ if (name === undefined)
593
1005
  return wrapper;
594
- return declareSynthetic?.(name, () => wrapper) ?? wrapper;
1006
+ return declareSynthetic?.(name, () => wrapper, union) ?? wrapper;
1007
+ }
1008
+ /**
1009
+ * **The component openapi3 publishes an envelope as**: the union's name and the variant's, suffixed
1010
+ * with the envelope's visibility where the union - whose usage openapi3 gives the envelope - is used
1011
+ * at more than one. None for a union with no name, whose envelopes are written out where it is.
1012
+ */
1013
+ export function envelopeNameOf(program, union, variantName, variant) {
1014
+ if (union.name === undefined)
1015
+ return undefined;
1016
+ const envelopeAt = envelopeVisibilityOf(program, union, variant);
1017
+ const suffixed = envelopeAt !== Visibility.Read && isSuffixedByUsage(union);
1018
+ return `${union.name}${capitalise(variantName)}${suffixed ? getVisibilitySuffix(envelopeAt, Visibility.Read) : ""}`;
595
1019
  }
596
1020
  function unionToZod(program, union) {
597
1021
  /**
@@ -619,43 +1043,151 @@ function unionToZod(program, union) {
619
1043
  * reader. A validator may only enforce what the document says.
620
1044
  */
621
1045
  if (options.envelope === "none") {
622
- const undeclared = [...discriminated.variants.values()].filter((variant) => !declaresDiscriminator(program, variant, options.discriminatorPropertyName));
623
- if (undeclared.length > 0) {
1046
+ const undeclared = [...discriminated.variants.values()]
1047
+ .map((variant) => ({
1048
+ variant,
1049
+ why: undeclaredVariantOf(program, variant, options.discriminatorPropertyName),
1050
+ }))
1051
+ .filter(({ why }) => why === "missing" || why === "open");
1052
+ const [first] = undeclared;
1053
+ if (first !== undefined) {
624
1054
  reportDiagnostic(program, {
625
1055
  code: "undeclared-discriminator",
1056
+ messageId: first.why === "open" ? "open" : "default",
626
1057
  target: union,
627
1058
  format: {
628
1059
  name: union.name ?? "an anonymous union",
629
1060
  property: options.discriminatorPropertyName,
630
- variant: undeclared[0]?.name ?? "one of its variants",
1061
+ variant: first.variant.name ?? "one of its variants",
631
1062
  },
632
1063
  });
633
1064
  return UNREPRESENTABLE;
634
1065
  }
635
1066
  }
636
- const members = [...discriminated.variants.entries()].map(([name, variant]) => discriminatedVariant(program, union, name, variant, options));
1067
+ const dispatched = dispatchedVariantsOf(program, union);
1068
+ // A variant left out for a key its sealed base refuses is refused as itself, as anywhere else.
1069
+ for (const variant of discriminated.variants.values()) {
1070
+ if (variant.kind === "Model")
1071
+ reportSealedBaseExtended(program, variant);
1072
+ }
1073
+ const kept = dispatched
1074
+ .map(([name, variant]) => ({
1075
+ variant,
1076
+ member: discriminatedVariant(program, union, name, variant, options),
1077
+ }))
1078
+ .filter(({ member }) => !held.refused(member));
1079
+ const members = kept.map(({ member }) => member);
1080
+ // Every variant refused by a declaration above it: the document accepts no value either.
1081
+ if (members.length === 0)
1082
+ return UNREPRESENTABLE;
637
1083
  const property = options.discriminatorPropertyName;
638
- return `z.discriminatedUnion(${literal(property)}, [${members.join(", ")}])`;
1084
+ /**
1085
+ * **A discriminator a PATCH makes optional dispatches nothing**: `z.discriminatedUnion` reads
1086
+ * one fixed value per member, and the document publishes `oneOf` - exactly one member - which
1087
+ * with the tag absent is what `z.xor` checks. As a dispatch, `{ value: {...} }` was refused.
1088
+ */
1089
+ if (dispatched.some(([, variant]) => optionalDiscriminator(program, union, variant, options))) {
1090
+ return members.length === 1 ? members[0] : `z.xor([${members.join(", ")}])`;
1091
+ }
1092
+ /**
1093
+ * **A variant no dispatch can hold sits beside it**, as a nested base does beside a base's own
1094
+ * dispatch: one with no envelope that is a `@discriminator` base itself, a `z.union` with no
1095
+ * literal to read, and one annotated for its recursion. In the dispatch, every parse threw
1096
+ * "Invalid discriminated union option". Each fixes a value of its own, so at most one matches.
1097
+ */
1098
+ const beside = kept
1099
+ .filter(({ variant, member }) => held.annotated(member) ||
1100
+ (options.envelope === "none" &&
1101
+ variant.kind === "Model" &&
1102
+ discriminatedBaseOf(program, variant) !== undefined))
1103
+ .map(({ member }) => member);
1104
+ const dispatch = members.filter((member) => !beside.includes(member));
1105
+ if (beside.length === 0) {
1106
+ return `z.discriminatedUnion(${literal(property)}, [${members.join(", ")}])`;
1107
+ }
1108
+ const arms = [
1109
+ ...(dispatch.length === 0
1110
+ ? []
1111
+ : [`z.discriminatedUnion(${literal(property)}, [${dispatch.join(", ")}])`]),
1112
+ ...beside,
1113
+ ];
1114
+ return arms.length === 1 ? arms[0] : `z.xor([${arms.join(", ")}])`;
639
1115
  }
640
1116
  const variants = [...union.variants.values()].map((variant) => variant.type);
641
- const nullable = variants.some((type) => type.kind === "Intrinsic" && type.name === "null");
642
- const real = variants.filter((type) => !(type.kind === "Intrinsic" && type.name === "null"));
643
- if (real.length === 0) {
644
- reportDiagnostic(program, {
645
- code: "empty-union",
646
- target: union,
647
- format: { why: "every variant is `null`" },
648
- });
649
- return UNREPRESENTABLE;
650
- }
651
- const suffix = nullable ? ".nullable()" : "";
652
- if (real.every((type) => type.kind === "String")) {
653
- const literals = real.map((type) => literal(type.value));
654
- return `z.enum([${literals.join(", ")}])${suffix}`;
1117
+ const nulls = variants.filter(isNull).length;
1118
+ const real = variants.filter((type) => !isNull(type));
1119
+ const exclusive = isExclusive(program, union) || exclusiveSite === union;
1120
+ /**
1121
+ * **Asked once the members are written**: writing one declares it, and a declaration openapi3
1122
+ * publishes split is refused only as it is written. Asked first, `@oneOf { v0: float, v1: M10, v2:
1123
+ * M11 }` held the `M10` refused while it was written, and the contract type, walked after every
1124
+ * refusal, was `never` - `wire-contract.gen.ts` did not compile (`TS2344`).
1125
+ */
1126
+ const written = (() => {
1127
+ // A raw `bytes` member of a multipart part's union is any value - see `withMultipartBinary`.
1128
+ const memberToZod = (type) => multipartBinary && isRawBytes(program, type) ? "z.unknown()" : typeToZod(program, type);
1129
+ /**
1130
+ * **A `@oneOf` refuses the `null` more than one of its members matches**: openapi3 publishes
1131
+ * each `null` variant as a `{ type: "null" }` of its own, beside members that may admit it too -
1132
+ * `unknown | null` is `oneOf [{}, null]`, which no `null` passes. Each is a `z.null()` member of
1133
+ * the `z.xor`, which counts as the document does; `.nullable()` passed `null` before either.
1134
+ */
1135
+ if (exclusive &&
1136
+ nulls > 0 &&
1137
+ (nulls > 1 || real.some((type) => admitsNullValue(program, type)))) {
1138
+ const members = [...literalGroupsOf(real, memberToZod), ...Array(nulls).fill("z.null()")];
1139
+ return `z.xor([${members.join(", ")}])`;
1140
+ }
1141
+ // `null | null` is `null`, as the document reads it - and `q: null` was never refused.
1142
+ if (real.length === 0)
1143
+ return "z.null()";
1144
+ const suffix = nulls > 0 ? ".nullable()" : "";
1145
+ if (real.every((type) => type.kind === "String")) {
1146
+ const literals = real.map((type) => literal(type.value));
1147
+ return `z.enum([${literals.join(", ")}])${suffix}`;
1148
+ }
1149
+ if (real.length === 1)
1150
+ return `${memberToZod(real[0])}${suffix}`;
1151
+ /**
1152
+ * **Every literal of one JSON kind is one member of a `@oneOf`**, as openapi3 publishes it - one
1153
+ * `enum` per kind: `1` repeated is matched once, where two `z.literal(1)` matched it twice.
1154
+ */
1155
+ const members = exclusive ? literalGroupsOf(real, memberToZod) : real.map(memberToZod);
1156
+ if (members.length === 1)
1157
+ return `${members[0]}${suffix}`;
1158
+ return `${exclusive ? "z.xor" : "z.union"}([${members.join(", ")}])${suffix}`;
1159
+ })();
1160
+ return holdsRefusedMember(program, union, exclusive) ? UNREPRESENTABLE : written;
1161
+ }
1162
+ /** A union's members as Zod, its literals of each JSON kind as one member where the first one stands. */
1163
+ function literalGroupsOf(types, memberToZod) {
1164
+ const groups = new Map();
1165
+ const members = [];
1166
+ for (const type of types) {
1167
+ const value = type.kind === "EnumMember" ? undefined : literalValueOf(type);
1168
+ if (value === undefined) {
1169
+ members.push(memberToZod(type));
1170
+ continue;
1171
+ }
1172
+ const kind = typeof value;
1173
+ const group = groups.get(kind);
1174
+ if (group === undefined) {
1175
+ groups.set(kind, [value]);
1176
+ members.push({ group: kind });
1177
+ }
1178
+ else if (!group.includes(value))
1179
+ group.push(value);
655
1180
  }
656
- if (real.length === 1)
657
- return `${typeToZod(program, real[0])}${suffix}`;
658
- return `z.union([${real.map((type) => typeToZod(program, type)).join(", ")}])${suffix}`;
1181
+ return members.map((member) => {
1182
+ if (typeof member === "string")
1183
+ return member;
1184
+ const values = groups.get(member.group) ?? [];
1185
+ if (values.every((each) => typeof each === "string")) {
1186
+ return `z.enum([${values.map((each) => literal(each)).join(", ")}])`;
1187
+ }
1188
+ const literals = values.map((each) => `z.literal(${literal(each)})`);
1189
+ return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
1190
+ });
659
1191
  }
660
1192
  /**
661
1193
  * TypeSpec models arrays and dictionaries as a `Model` carrying an **indexer**.
@@ -694,230 +1226,1858 @@ function isUnknownType(type) {
694
1226
  * refers back to the derived model - `type/model/inheritance/recursive`, the exact case - would throw
695
1227
  * during module initialisation. Flattening asks nothing of the base but its properties.
696
1228
  */
697
- export function inheritedAndOwnProperties(model) {
1229
+ export function inheritedAndOwnProperties(program, model) {
1230
+ return flattenedPropertiesOf(program, model).map((entry) => entry.property);
1231
+ }
1232
+ export function flattenedPropertiesOf(program, model) {
698
1233
  const chain = [];
699
1234
  for (let link = model; link !== undefined; link = link.baseModel) {
700
1235
  chain.unshift(link);
701
1236
  }
702
- // Base first, so a derived model REDECLARING a property overrides rather than duplicates it.
703
- const byName = new Map();
1237
+ // Base first, so a derived model REDECLARING a key overrides rather than duplicates it.
1238
+ const byKey = new Map();
704
1239
  for (const link of chain) {
705
- for (const property of link.properties.values())
706
- byName.set(property.name, property);
1240
+ for (const property of link.properties.values()) {
1241
+ const key = rawPropertyKey(program, property);
1242
+ const declarations = byKey.get(key) ?? [];
1243
+ // Two keys of one name in one model: openapi3 writes the later over the earlier.
1244
+ if (declarations.at(-1)?.model === link)
1245
+ declarations.pop();
1246
+ declarations.push({ model: link, property });
1247
+ byKey.set(key, declarations);
1248
+ }
707
1249
  }
708
- return [...byName.values()];
1250
+ return [...byKey.values()].map((declarations) => ({
1251
+ property: declarations.at(-1).property,
1252
+ overridden: declarations
1253
+ .slice(0, -1)
1254
+ .reverse()
1255
+ .map((declaration) => declaration.property),
1256
+ }));
709
1257
  }
710
- /** The indexer in force, which a model can inherit - `model X extends Record<unknown>`. */
711
- export function effectiveIndexer(model) {
712
- for (let link = model; link !== undefined; link = link.baseModel) {
713
- if (link.indexer !== undefined)
714
- return link.indexer;
1258
+ /**
1259
+ * The keys a model publishes at the current position, each with every declaration of it the
1260
+ * document holds the value to - its most derived published declaration first.
1261
+ *
1262
+ * **A declaration counts where openapi3 publishes it, and only there.** Each model in the chain is
1263
+ * published as `allOf` the one above, and each judges its own properties at the position: a
1264
+ * `@header` or a create-only property above a response is in no schema the response is held to, so
1265
+ * its `@maxLength` bounds nothing there; a `never` or an invisible redeclaration drops out of its own
1266
+ * model's schema and leaves the key to the ones above it. Judged on the most derived declaration
1267
+ * alone, the first was enforced where the document does not, and the second dropped a key the base
1268
+ * still requires.
1269
+ *
1270
+ * **HTTP metadata is not payload.** A `@header` or `@statusCode` property describes the envelope, so
1271
+ * it never appears in the body: RFC 9457 puts `@header("content-type") contentType:
1272
+ * "application/problem+json"` on the problem model, the document described a body without it, and
1273
+ * this emitter required `contentType` as a member. **A `never` property is dropped, as the document
1274
+ * drops it**: `model N { value: never; other: string }` is published as `{other}`, and a validator
1275
+ * keeping `value: z.never()` refused `{"other": "x"}`, the exact body the document describes.
1276
+ */
1277
+ export function publishedPropertiesOf(program, model) {
1278
+ return declarationsWhere(program, model, (declaration) => atPublication(program, declaration, () => isPayloadProperty(program, declaration)));
1279
+ }
1280
+ /**
1281
+ * **The keys a model's `required` is read from: each declaration at the visibility its model is
1282
+ * rendered at**, where openapi3 reduces the reference - see `contentMisreadingsOf`. Wider than
1283
+ * {@link publishedPropertiesOf} where a read-only declaration drops out of the properties openapi3
1284
+ * reads at a create body's visibility, and stays required at `Read`.
1285
+ */
1286
+ function renderedPropertiesOf(program, model) {
1287
+ return declarationsWhere(program, model, (declaration) => atDeclaration(program, declaration, () => isPayloadProperty(program, declaration)));
1288
+ }
1289
+ function declarationsWhere(program, model, present) {
1290
+ const published = [];
1291
+ for (const entry of flattenedPropertiesOf(program, model)) {
1292
+ const [property, ...overridden] = [entry.property, ...entry.overridden].filter((declaration) => present(declaration) && !isNeverType(declaration.type));
1293
+ if (property !== undefined)
1294
+ published.push({ property, overridden });
715
1295
  }
716
- return undefined;
1296
+ return published;
717
1297
  }
718
1298
  /**
719
- * The subtypes a `@discriminator` base stands for, in declaration order.
720
- *
721
- * Filtered by openapi3's own {@link includeDerivedModel} so a template declaration is not mistaken
722
- * for a real subtype - the same rule that decides whether a base may be sealed.
1299
+ * **A declaration is in its model's schema where openapi3 reads that model's properties: at the
1300
+ * visibility the reference it writes the model from arrived at** - see `readVisibilityOf` - not the
1301
+ * one it reduces that to. `X { @visibility(Read) @maxLength(2) id: string; ...Record<string> }`
1302
+ * first sent in a create body is published without `id` in its properties, so its indexer holds the
1303
+ * `id` its `required` names; validated by the read-only declaration, `"abcdef"` was refused. Likewise
1304
+ * a base's `id`, or the `string` openapi3 adds for a discriminator, holds a key whose read-only
1305
+ * declaration drops out.
723
1306
  */
724
- export function discriminatedSubtypes(program, model) {
725
- if (getDiscriminator(program, model) === undefined)
726
- return [];
727
- return model.derivedModels.filter((derived) => includeDerivedModel(derived));
1307
+ function atPublication(program, property, run) {
1308
+ const model = property.model;
1309
+ if (model === undefined)
1310
+ return run();
1311
+ // Of several views under one name, the one the document keeps - see `keptVisibilityOf`.
1312
+ const rendered = keptVisibilityOf(model, renderedVisibilityOf(program, model, visibility));
1313
+ return withVisibility(readVisibilityOf(program, model, rendered, metadataIgnored), run);
728
1314
  }
729
- function modelToZod(program, model) {
1315
+ /**
1316
+ * The discriminators a model is published under: its own and every one up its chain, by wire name.
1317
+ * openapi3 lists each in its base's `required`, and the base applies through `allOf`.
1318
+ */
1319
+ export function discriminatorsOf(program, model) {
1320
+ const names = [];
1321
+ for (let link = model; link !== undefined; link = link.baseModel) {
1322
+ const discriminator = getDiscriminator(program, link);
1323
+ if (discriminator !== undefined && !names.includes(discriminator.propertyName)) {
1324
+ names.push(discriminator.propertyName);
1325
+ }
1326
+ }
1327
+ return names;
1328
+ }
1329
+ export const OWN_POSITION = {
1330
+ constrainedBy: [],
1331
+ required: false,
1332
+ stringOnly: false,
1333
+ never: false,
1334
+ nullOnly: false,
1335
+ nonNull: false,
1336
+ value: undefined,
1337
+ ancestors: [],
1338
+ refused: undefined,
1339
+ };
1340
+ export function positionOf(program, model, entry) {
1341
+ const key = rawPropertyKey(program, entry.property);
1342
+ return positionOfDeclarations(program, entry.property, entry.overridden, {
1343
+ required: discriminatorsOf(program, model).includes(key),
1344
+ injected: injectedDiscriminatorsOf(program, model).includes(key),
1345
+ beside: [],
1346
+ });
1347
+ }
1348
+ /**
1349
+ * The position of `own` beneath `ancestors`, the declarations of one key the document holds its
1350
+ * value to, plus `beside`: what no declaration states - a typed indexer of a model merged with it, an
1351
+ * envelope's members.
1352
+ */
1353
+ function positionOfDeclarations(program, own, ancestors, options) {
1354
+ const position = {
1355
+ ...OWN_POSITION,
1356
+ ancestors,
1357
+ required: options.required || ancestors.some((ancestor) => isRequiredAt(program, ancestor)),
1358
+ ...(options.at?.[0] === undefined ? {} : { at: options.at[0] }),
1359
+ };
1360
+ const beside = options.beside.filter((part) => !isTypelessPart(program, part) || part.keywords.length > 0);
1361
+ const atOf = (index) => options.at?.[index];
1362
+ const ownAt = atOf(0) ?? visibility;
730
1363
  /**
731
- * **A `@discriminator` base is a CHOICE between its subtypes, not a shape of its own.**
732
- *
733
- * `@discriminator("kind") model Bird { kind: string; wingspan: int32 }` with four models extending
734
- * it reaches OpenAPI as a component carrying `discriminator: {propertyName, mapping}`, and the
735
- * mapping is an instruction: validate the body against whichever subtype the discriminator names.
736
- * Emitting the base's own properties instead produced `z.object({kind: z.string(), wingspan})`,
737
- * which accepts `{kind: "eagle"}` while ignoring everything `Eagle` actually declares - a
738
- * polymorphic endpoint validating none of its polymorphism.
739
- *
740
- * `z.discriminatedUnion` is the faithful encoding and composes both ways this corpus needs.
741
- * Measured on Zod 4.4.3: a union may be an OPTION of another union, which is how
742
- * `Fish -> Shark -> SawShark` works; and an option carrying a getter that refers back to the union
743
- * being defined does **not** throw, because the discriminator is read without enumerating the rest
744
- * of the shape. `Eagle.friends?: Bird[]` is exactly that, and it decides whether polymorphism and
745
- * recursion can coexist at all.
1364
+ * **A key one declaration states is still published at that declaration's visibility**: `M5`'s
1365
+ * `e: M6`, merged at `Read` beside `M4Update`, is `M6`, and rendered at the merge's own `Update`
1366
+ * it was `M6Update`, which accepts the `{}` that `M6` refuses.
746
1367
  */
747
- const subtypes = discriminatedSubtypes(program, model);
748
- if (subtypes.length > 0) {
749
- // `discriminatedSubtypes` is empty unless the model has a discriminator.
750
- const discriminator = getDiscriminator(program, model)?.propertyName ?? "";
751
- const options = subtypes.map((derived) => typeToZod(program, derived));
752
- return `z.discriminatedUnion(${literal(discriminator)}, [${options.join(", ")}])`;
1368
+ const apart = ownAt !== visibility &&
1369
+ renderedVisibilityOf(program, nullableInnerOf(own.type) ?? own.type, ownAt) !==
1370
+ renderedVisibilityOf(program, nullableInnerOf(own.type) ?? own.type, visibility);
1371
+ if (ancestors.length === 0 && !options.injected && beside.length === 0 && !apart) {
1372
+ return position;
753
1373
  }
754
- const indexer = effectiveIndexer(model);
755
- const indexerKey = indexer?.key.name;
756
- const declared = inheritedAndOwnProperties(model);
757
- if (indexerKey === "integer" && indexer !== undefined) {
758
- return applyConstraints(program, `z.array(${typeToZod(program, indexer.value)})`, model);
1374
+ // Reported and refused by its own walk - see `unsatisfiable-encoding`.
1375
+ if (encodedValueOf(program, own).kind === "unsatisfiable")
1376
+ return position;
1377
+ const constrainedBy = [];
1378
+ let never = false;
1379
+ let nullOnly = false;
1380
+ let nonNull = false;
1381
+ let merge = beside.length > 0 || apart;
1382
+ for (const [index, ancestor] of ancestors.entries()) {
1383
+ /**
1384
+ * **A declaration published at another visibility is merged, unless its type is one schema at
1385
+ * both**: `In` in a list literal's element is published at `Update | Item` and in a named list's
1386
+ * at `Update`, and taken as narrowed by itself it was `InUpdate` alone, `x` optional where the
1387
+ * element requires it.
1388
+ */
1389
+ const ancestorAt = atOf(index + 1) ?? visibility;
1390
+ const declared = nullableInnerOf(ancestor.type) ?? ancestor.type;
1391
+ if (ancestorAt !== ownAt &&
1392
+ renderedVisibilityOf(program, declared, ancestorAt) !==
1393
+ renderedVisibilityOf(program, declared, ownAt)) {
1394
+ merge = true;
1395
+ continue;
1396
+ }
1397
+ if (elementsPublishedApart(program, nullableInnerOf(own.type) ?? own.type, ownAt, declared, ancestorAt)) {
1398
+ merge = true;
1399
+ continue;
1400
+ }
1401
+ const verdict = overrideVerdictOf(program, own, ancestor);
1402
+ if (verdict.kind === "never")
1403
+ never = true;
1404
+ else if (verdict.kind === "null")
1405
+ nullOnly = true;
1406
+ else if (verdict.kind === "merge")
1407
+ merge = true;
1408
+ else {
1409
+ constrainedBy.push(...verdict.constrainedBy);
1410
+ nonNull ||= verdict.nonNull;
1411
+ }
759
1412
  }
760
- /**
761
- * **A string indexer only means "dictionary" when the model declares NO properties.**
762
- *
763
- * `model X { id: string; ...Record<unknown> }` - the standard TypeSpec way to say "these fields,
764
- * plus anything else", and what `@loose` was replaced by - carries both. Returning
765
- * `z.record(...)` for it emits a schema that accepts an object and then **strips every declared
766
- * field**, so `respond()`'s drift detection degrades to "is it an object" and the pinned fields
767
- * silently stop being checked. This function's own history is the other half of the same mistake:
768
- * it once keyed on the model's *name*, and lost the indexer of anything derived from `Record`.
769
- */
770
- if (indexerKey === "string" && indexer !== undefined && declared.length === 0) {
771
- return applyConstraints(program, `z.record(z.string(), ${withDeclarationSite(model, () => typeToZod(program, indexer.value))})`, model);
1413
+ if (never)
1414
+ return { ...position, never: true };
1415
+ if (merge) {
1416
+ const withAt = (declaration, index) => {
1417
+ const part = partOfProperty(program, declaration);
1418
+ const at = atOf(index);
1419
+ return part === undefined || at === undefined ? part : { ...part, at };
1420
+ };
1421
+ const value = mergeParts(program, [
1422
+ ...[own, ...ancestors].map(withAt),
1423
+ ...(options.injected ? [stringPart(program)] : []),
1424
+ ...beside,
1425
+ ]);
1426
+ const refusal = refusalIn(value);
1427
+ return {
1428
+ ...position,
1429
+ value,
1430
+ never: value.kind === "never",
1431
+ refused: refusal === undefined
1432
+ ? undefined
1433
+ : {
1434
+ ancestor: ancestors[0] ?? own,
1435
+ why: refusal.why,
1436
+ recursive: refusal.recursive === true,
1437
+ },
1438
+ };
772
1439
  }
1440
+ let stringOnly = false;
1441
+ if (options.injected) {
1442
+ const types = valueJsonTypesOf(program, own);
1443
+ if (types === undefined)
1444
+ stringOnly = true;
1445
+ else if (!types.has("string"))
1446
+ never = true;
1447
+ else
1448
+ stringOnly = [...types].some((type) => type !== "string");
1449
+ nonNull ||= admitsNull(program, own);
1450
+ }
1451
+ return { ...position, constrainedBy, stringOnly, never, nullOnly, nonNull };
1452
+ }
1453
+ /** Whether a declaration requires its key at the current position. */
1454
+ function isRequiredAt(program, property) {
1455
+ return !(property.optional || atDeclaration(program, property, () => isOptionalAt(program, property)));
1456
+ }
1457
+ /**
1458
+ * **A declaration is judged at the visibility its own model is rendered at**, as openapi3 renders
1459
+ * each model of a chain: a subtype's create body is `allOf` its base, and a base with nothing
1460
+ * create-specific is the canonical schema - its read-only `id` shared in, `readOnly` and required.
1461
+ * Judged at the subtype's visibility, the base's `id` dropped out of the create body the document
1462
+ * requires it in. See `renderedVisibilityOf`.
1463
+ */
1464
+ /** At the visibility a merged member's declaration is published at, where it is not the merge's. */
1465
+ export function atPosition(position, run) {
1466
+ return position.at === undefined ? run() : withVisibility(position.at, run);
1467
+ }
1468
+ export function atDeclaration(program, property, run) {
1469
+ const model = property.model;
1470
+ // Of several views under one name, the one the document keeps - see `keptVisibilityOf`.
1471
+ return model === undefined
1472
+ ? run()
1473
+ : withVisibility(keptVisibilityOf(model, renderedVisibilityOf(program, model, visibility)), run);
1474
+ }
1475
+ /**
1476
+ * **A property's type is referred to at the visibility openapi3 reads its declaration's properties
1477
+ * at** - see `readVisibilityOf` - where the declaration is rendered at the current one. Its
1478
+ * optionality and its position stay where the model is published; only what the type is named, a
1479
+ * list or a union instance openapi3 does not reduce, can differ.
1480
+ */
1481
+ export function atReading(program, property, run) {
1482
+ const model = property.model;
1483
+ if (model === undefined)
1484
+ return run();
1485
+ const read = readVisibilityOf(program, model, visibility, metadataIgnored);
1486
+ return read === visibility ? run() : withVisibility(read, run);
1487
+ }
1488
+ /** Whether a declaration's value may be `null`, as the document publishes it. */
1489
+ function admitsNull(program, property) {
1490
+ const encoding = encodedValueOf(program, property);
1491
+ if (encoding.kind === "replaced" || encoding.kind === "both")
1492
+ return encoding.nullable;
1493
+ if (encoding.kind === "unsatisfiable")
1494
+ return false;
1495
+ return admitsNullValue(program, property.type, exclusiveAt(program, property));
1496
+ }
1497
+ /** Whether a property's value is a `@oneOf`: its union's own, or its own on its inline union. */
1498
+ function exclusiveAt(program, property) {
1499
+ const type = property.type;
1500
+ return (isExclusive(program, type) ||
1501
+ (isExclusive(program, property) && type.kind === "Union" && type.name === undefined));
1502
+ }
1503
+ /**
1504
+ * **`T | null` read as `T` beside its `null`** - unless it is a `@oneOf` whose `T` takes `null`
1505
+ * too, which then refuses it: `@oneOf unknown | null` is `oneOf [{}, null]`, and no `null` passes.
1506
+ */
1507
+ function peeledNullOf(program, property) {
1508
+ const inner = nullableInnerOf(property.type);
1509
+ return inner !== undefined && exclusiveAt(program, property) && admitsNullValue(program, inner)
1510
+ ? undefined
1511
+ : inner;
1512
+ }
1513
+ /**
1514
+ * **Whether two lists' elements are published at visibilities their types differ at**: a list
1515
+ * literal's element at `Item`, a named list's at the list's own. Under a PATCH, `u: El[]` over
1516
+ * `u: Els` holds each element to `El` and to `ElUpdate`, and taken as narrowed by the element type
1517
+ * alone it was `El`, sealed against the `c` that `InnerUpdate` refuses.
1518
+ */
1519
+ function elementsPublishedApart(program, own, ownAt, ancestor, ancestorAt) {
1520
+ const elementOf = (list, at) => {
1521
+ if (list.kind !== "Model" || effectiveIndexer(list)?.key.name !== "integer")
1522
+ return undefined;
1523
+ const element = effectiveIndexer(list).value;
1524
+ return { element, at: isArrayLiteral(program, list) ? at | Visibility.Item : at };
1525
+ };
1526
+ const one = elementOf(own, ownAt);
1527
+ const other = elementOf(ancestor, ancestorAt);
1528
+ if (one === undefined || other === undefined || one.at === other.at)
1529
+ return false;
1530
+ return [one.element, other.element].some((element) => renderedVisibilityOf(program, element, one.at) !==
1531
+ renderedVisibilityOf(program, element, other.at));
1532
+ }
1533
+ /** How one declaration above an overriding one bears on its value - see {@link PropertyPosition}. */
1534
+ function overrideVerdictOf(program, own, ancestor) {
1535
+ if (disjoint(program, own, ancestor))
1536
+ return { kind: "never" };
1537
+ const ancestorEncoding = encodedValueOf(program, ancestor);
1538
+ if (ancestorEncoding.kind === "unsatisfiable")
1539
+ return { kind: "never" };
1540
+ const ownEncoding = encodedValueOf(program, own);
1541
+ // An encoding that keeps its declared type beside another holds the value to both, whole.
1542
+ if (ownEncoding.kind === "both" || ancestorEncoding.kind === "both")
1543
+ return { kind: "merge" };
773
1544
  /**
774
- * Permissiveness, read from the INDEXER and from nothing else.
775
- *
776
- * `...Record<unknown>` is the standard TypeSpec construct and the one `@typespec/openapi3` can
777
- * see, so it is the only source. There was a second - `@loose`, a decorator of ours, invisible to
778
- * the document - and it is gone: a model that accepted unknown properties while the published
779
- * schema said otherwise is a runtime disagreeing with its own contract.
780
- *
781
- * An indexer over something other than `unknown` is a typed catchall, where `.loose()` would drop
782
- * that value type on the floor.
783
- */
784
- /**
785
- * **`Record<never>` is how a spec says "and nothing else", not a value type to walk.**
786
- *
787
- * `model A { ...Record<never>; name: string }` gives an indexer whose value is the `never`
788
- * intrinsic. Read as an ordinary typed catchall it became `.catchall(z.never())`, which reached
789
- * `typeToZod` and refused the whole compile - while `@typespec/openapi3` publishes the same model
790
- * cleanly as `additionalProperties: {not: {}}`, with no diagnostic at all. `isSealed` already reads
791
- * a `never` indexer as sealed; this stops the catchall branch consuming it first.
792
- */
793
- const indexerValue = indexerKey === "string" && indexer !== undefined && !isNeverType(indexer.value)
794
- ? indexer.value
795
- : undefined;
796
- const suffix = indexerValue !== undefined && isUnknownType(indexerValue)
797
- ? ".loose()"
798
- : indexerValue === undefined
799
- ? ""
800
- : `.catchall(${withDeclarationSite(model, () => typeToZod(program, indexerValue))})`;
801
- /**
802
- * A request model REJECTS an unrecognised field rather than stripping it.
803
- *
804
- * Silently dropping something a caller sent is the same failure this whole surface exists to
805
- * remove, one layer out: the request succeeds and does less than it was asked to, with nothing
806
- * anywhere reporting it. `appointedOn` went missing for exactly that reason and surfaced as a
807
- * confusing downstream 400 instead of a loud one naming the key.
1545
+ * `null` is a value of its own: no keyword applies to it, and it is valid on both sides only
1546
+ * where both admit it. An override of `null` alone narrows an ancestor admitting it.
808
1547
  */
809
- const strict = suffix === "" && isSealed(model) ? ".strict()" : "";
1548
+ const ownNull = admitsNull(program, own);
1549
+ const ancestorNull = admitsNull(program, ancestor);
1550
+ const nonNull = ownNull && !ancestorNull;
1551
+ const implied = { kind: "implied", constrainedBy: [], nonNull };
1552
+ if (isNull(own.type))
1553
+ return ancestorNull ? implied : { kind: "never" };
810
1554
  /**
811
- * **HTTP metadata is not payload.** A `@header` or `@statusCode` property describes the envelope,
812
- * so it never appears in the body - `@typespec/openapi3` strips it from the schema it emits, and a
813
- * validator that kept it would demand a field the wire never carries.
814
- *
815
- * Found the moment a model carried its own content-type: RFC 9457 puts
816
- * `@header("content-type") contentType: "application/problem+json"` on the problem model, the
817
- * document correctly described a body without it, and this emitter produced
818
- * `contentType: z.literal(...)` as a required member. Two artefacts from one source, disagreeing
819
- * about the shape of a body - the exact contradiction ADR 0005 exists to make impossible.
1555
+ * A literal is judged now, value by value: against the ancestor's constraints and its declared
1556
+ * type, as JSON Schema would. Every value admitted leaves nothing to apply; none admitted leaves
1557
+ * `null` where both sides allow it, and no value otherwise. Values some of which the ancestor's
1558
+ * own keywords refuse are narrowed member by member - see `constrainedMembersOf` - and values its
1559
+ * TYPE refuses are merged out.
820
1560
  */
1561
+ const literals = literalValuesOfType(own.type);
1562
+ if (literals !== undefined) {
1563
+ const ancestorType = ancestorEncoding.kind === "replaced" ? ancestorEncoding.scalar : ancestor.type;
1564
+ const byType = literals.filter((value) => admitsValue(program, ancestorType, value));
1565
+ const admitted = byType.filter((value) => brokenConstraintOf(program, ancestor, value) === undefined);
1566
+ if (admitted.length === literals.length)
1567
+ return implied;
1568
+ if (admitted.length === 0) {
1569
+ return ownNull && ancestorNull ? { kind: "null" } : { kind: "never" };
1570
+ }
1571
+ if (byType.length === literals.length) {
1572
+ return { kind: "implied", constrainedBy: [ancestor], nonNull };
1573
+ }
1574
+ return { kind: "merge" };
1575
+ }
1576
+ const ownType = ownEncoding.kind === "replaced" ? ownEncoding.scalar : (peeledNullOf(program, own) ?? own.type);
1577
+ const ancestorType = ancestorEncoding.kind === "replaced"
1578
+ ? ancestorEncoding.scalar
1579
+ : (peeledNullOf(program, ancestor) ?? ancestor.type);
1580
+ const narrowed = narrowsTo(program, ownType, ancestorType);
1581
+ if (narrowed === undefined)
1582
+ return { kind: "merge" };
1583
+ return {
1584
+ kind: "implied",
1585
+ constrainedBy: [...(hasConstraints(program, ancestor) ? [ancestor] : []), ...narrowed],
1586
+ nonNull,
1587
+ };
1588
+ }
1589
+ /**
1590
+ * **Whether every value of `own` is a value of `ancestor`**, and the declared scalars whose rules
1591
+ * are left to apply - or `undefined` where neither one validator can say. A scalar narrows by
1592
+ * TypeSpec's own assignability, plus the rules of any declared scalar above it; a model only by
1593
+ * extending; a list by its element; a union where some member does.
1594
+ */
1595
+ function narrowsTo(program, own, ancestor) {
1596
+ if (own === ancestor)
1597
+ return [];
1598
+ if (own.kind === "UnionVariant")
1599
+ return narrowsTo(program, own.type, ancestor);
1600
+ if (ancestor.kind === "UnionVariant")
1601
+ return narrowsTo(program, own, ancestor.type);
1602
+ // A union with an envelope is published as its wrappers, which no variant type is.
1603
+ if (isEnveloped(program, own) || isEnveloped(program, ancestor))
1604
+ return undefined;
1605
+ // A literal narrows whatever admits its value, read statically.
1606
+ const value = literalValueOf(own);
1607
+ if (value !== undefined)
1608
+ return admitsValue(program, ancestor, value) ? [] : undefined;
1609
+ if (own.kind === "Union") {
1610
+ const scalars = [];
1611
+ for (const member of membersOf(own)) {
1612
+ // A `null` the ancestor refuses - a list's element, a record's value - is merged out.
1613
+ if (isNull(member)) {
1614
+ if (hasNull(ancestor) || isTypelessMember(program, ancestor))
1615
+ continue;
1616
+ return undefined;
1617
+ }
1618
+ const each = narrowsTo(program, member, ancestor);
1619
+ if (each === undefined)
1620
+ return undefined;
1621
+ scalars.push(...each);
1622
+ }
1623
+ return scalars;
1624
+ }
1625
+ switch (ancestor.kind) {
1626
+ case "Scalar": {
1627
+ if (own.kind !== "Scalar")
1628
+ return undefined;
1629
+ if (!isAssignable(program, own, ancestor) && !checksNoMoreThan(program, own, ancestor)) {
1630
+ return undefined;
1631
+ }
1632
+ return program.checker.isStdType(ancestor) ? [] : [ancestor];
1633
+ }
1634
+ case "Tuple": {
1635
+ if (own.kind !== "Tuple" || own.values.length !== ancestor.values.length)
1636
+ return undefined;
1637
+ const each = own.values.map((value, index) => narrowsTo(program, value, ancestor.values[index]));
1638
+ return each.every((value) => value !== undefined && value.length === 0) ? [] : undefined;
1639
+ }
1640
+ case "Model": {
1641
+ // A record of what narrows the ancestor's record's values, with no rules of its own to add.
1642
+ if (ancestor.indexer?.key.name === "string" &&
1643
+ ancestor.properties.size === 0 &&
1644
+ own.kind === "Model" &&
1645
+ own.indexer?.key.name === "string" &&
1646
+ own.properties.size === 0) {
1647
+ const value = narrowsTo(program, own.indexer.value, ancestor.indexer.value);
1648
+ return value !== undefined && value.length === 0 ? [] : undefined;
1649
+ }
1650
+ const ancestorElement = ancestor.indexer?.key.name === "integer" ? ancestor.indexer.value : undefined;
1651
+ if (ancestorElement !== undefined) {
1652
+ const ownElement = own.kind === "Model" && own.indexer?.key.name === "integer"
1653
+ ? own.indexer.value
1654
+ : undefined;
1655
+ if (ownElement === undefined)
1656
+ return undefined;
1657
+ // An element's own rules cannot be added to a list's: only a list of what it already is.
1658
+ const element = narrowsTo(program, ownElement, ancestorElement);
1659
+ if (element === undefined || element.length > 0)
1660
+ return undefined;
1661
+ // A named list model's own `@minItems` and `@maxItems` still hold - `b: Duo` under `Pair`.
1662
+ return hasConstraints(program, ancestor) ? [ancestor] : [];
1663
+ }
1664
+ for (let link = own.kind === "Model" ? own : undefined; link; link = link.baseModel) {
1665
+ if (link === ancestor)
1666
+ return [];
1667
+ }
1668
+ return undefined;
1669
+ }
1670
+ /**
1671
+ * **Into a union, only where the member it narrows says everything left to apply.** A member it
1672
+ * narrows wholly - itself, or a standard scalar - leaves nothing; ONE member it narrows with
1673
+ * rules of its own leaves those. Two such members are two alternatives - `Digits2` under
1674
+ * `Lower | Digits` is a `Digits2` that is a `Lower` OR a `Digits` - which no list of rules says,
1675
+ * so the declarations are merged member by member instead. Taking the first, `Digits2` was held
1676
+ * to `Lower`'s pattern and `Digits`' at once, and accepted nothing.
1677
+ */
1678
+ case "Union":
1679
+ case "Enum": {
1680
+ // Into `@oneOf`, a value of the override may match two members, which the document refuses.
1681
+ if (isExclusive(program, ancestor))
1682
+ return undefined;
1683
+ /**
1684
+ * **Into a `@discriminated` union - a `oneOf` too - only where its discriminator tells every
1685
+ * variant apart.** Where one may leave it out - `kind?`, or any under a PATCH - `{}` is an `NA`
1686
+ * and an `NB`, which the document refuses: narrowed to `NA` under `DN`, it was accepted.
1687
+ */
1688
+ if (ancestor.kind === "Union") {
1689
+ const [discriminated] = getDiscriminatedUnion(program, ancestor);
1690
+ if (discriminated !== undefined &&
1691
+ [...discriminated.variants.values()].some((variant) => optionalDiscriminator(program, ancestor, variant, discriminated.options))) {
1692
+ return undefined;
1693
+ }
1694
+ }
1695
+ const members = alikeMembersOf(program, ancestor);
1696
+ const narrowed = members.map((member) => narrowsTo(program, own, member));
1697
+ if (narrowed.some((each) => each !== undefined && each.length === 0))
1698
+ return [];
1699
+ const narrowing = narrowed.flatMap((each, index) => (each === undefined ? [] : [index]));
1700
+ if (narrowing.length !== 1)
1701
+ return undefined;
1702
+ const [index] = narrowing;
1703
+ /**
1704
+ * The one member's rules say everything only where no other member takes a value of it too:
1705
+ * `string` under `"green" | Short` is `"green"` OR a `Short`, and held to `Short`'s
1706
+ * `@maxLength` alone it refused the `"green"` both declarations accept.
1707
+ */
1708
+ return members.some((member, other) => other !== index && overlaps(program, own, member))
1709
+ ? undefined
1710
+ : narrowed[index];
1711
+ }
1712
+ case "Intrinsic":
1713
+ return ancestor.name === "unknown" ? [] : undefined;
1714
+ default:
1715
+ return undefined;
1716
+ }
1717
+ }
1718
+ /**
1719
+ * Whether a value of `own` may be a value of `member` too, as far as their JSON types and a literal
1720
+ * tell - taken as so where either has no type to tell by.
1721
+ */
1722
+ function overlaps(program, own, member) {
1723
+ const value = literalValueOf(member);
1724
+ if (value !== undefined)
1725
+ return admitsValue(program, own, value);
1726
+ const ownTypes = jsonTypesOf(program, own);
1727
+ const memberTypes = jsonTypesOf(program, member);
1728
+ if (ownTypes === undefined || memberTypes === undefined)
1729
+ return true;
1730
+ const numeric = (type) => type === "integer" || type === "number" || type === "fraction";
1731
+ return [...ownTypes].some((one) => [...memberTypes].some((other) => one === other || (numeric(one) && numeric(other))));
1732
+ }
1733
+ /**
1734
+ * **Whether a scalar's own check says everything another's does**, where TypeSpec's assignability
1735
+ * does not: `int32` under `float`, whose schema is only `type: number`, and `offsetDateTime` under
1736
+ * `utcDateTime`, which are checked alike. The other's own keywords are still applied, as the rules
1737
+ * of a declared scalar above it.
1738
+ */
1739
+ function checksNoMoreThan(program, own, ancestor) {
1740
+ if (scalarEncodingOf(program, own) !== undefined)
1741
+ return false;
1742
+ if (scalarEncodingOf(program, ancestor) !== undefined)
1743
+ return false;
1744
+ const ownBase = standardBaseOf(program, own);
1745
+ const ancestorBase = standardBaseOf(program, ancestor);
1746
+ if (ownBase === undefined || ancestorBase === undefined)
1747
+ return false;
1748
+ const ownCheck = SCALARS[ownBase.name];
1749
+ const ancestorCheck = SCALARS[ancestorBase.name];
1750
+ if (ownCheck === undefined || ancestorCheck === undefined)
1751
+ return false;
1752
+ if (ownCheck === ancestorCheck)
1753
+ return true;
1754
+ const json = jsonTypeOfScalar(program, ancestorBase);
1755
+ return (json !== undefined && ancestorCheck === BARE_CHECKS[json] && isWithin(program, ownBase, json));
1756
+ }
1757
+ /** The schema that checks a JSON type and nothing more. */
1758
+ const BARE_CHECKS = {
1759
+ string: "z.string()",
1760
+ number: "z.number()",
1761
+ boolean: "z.boolean()",
1762
+ };
1763
+ /** The standard scalar a scalar is built on. */
1764
+ function standardBaseOf(program, scalar) {
1765
+ for (let link = scalar; link !== undefined; link = link.baseScalar) {
1766
+ if (program.checker.isStdType(link))
1767
+ return link;
1768
+ }
1769
+ return undefined;
1770
+ }
1771
+ /** A `@discriminated` union with an envelope, whose members the document publishes as wrappers. */
1772
+ function isEnveloped(program, type) {
1773
+ if (type.kind !== "Union")
1774
+ return false;
1775
+ const [discriminated] = getDiscriminatedUnion(program, type);
1776
+ return discriminated !== undefined && discriminated.options.envelope !== "none";
1777
+ }
1778
+ /**
1779
+ * **Whether a union reaches itself through union members alone**: `union string { int32, string }`
1780
+ * in a namespace, where the member `string` names the union and not the scalar, or `A { int32, B }`
1781
+ * beside `B { string, A }`. No property or list element stands between the union and its own
1782
+ * reference, so the schema openapi3 publishes, `anyOf: [int32, { $ref: string }]`, refers to itself
1783
+ * with no value passed through on the way, and JSON Schema leaves that undefined - measured, Ajv
1784
+ * overflows its stack on `5`, `"abc"` and `null` alike. TypeScript refuses the alias it would be,
1785
+ * and the walks here that flatten a union recursed until the compiler reported "Emitter crashed!
1786
+ * This is a bug." A union with an envelope wraps each member in an object, which a value does pass
1787
+ * through, so the walk stops there.
1788
+ */
1789
+ export function reachesItselfThroughUnions(program, union) {
1790
+ const seen = new Set();
1791
+ const visit = (each) => {
1792
+ if (isEnveloped(program, each) || seen.has(each))
1793
+ return false;
1794
+ seen.add(each);
1795
+ return [...each.variants.values()].some(({ type }) => {
1796
+ let member = type;
1797
+ while (member.kind === "UnionVariant")
1798
+ member = member.type;
1799
+ return member === union || (member.kind === "Union" && visit(member));
1800
+ });
1801
+ };
1802
+ return visit(union);
1803
+ }
1804
+ /** Refuse a union that reaches itself through union members alone - see `reachesItselfThroughUnions`. */
1805
+ export function refuseSelfReachingUnion(program, union) {
1806
+ refuse(program, union, `its members reach '${getTypeName(union)}' again through unions alone, so the schema '@typespec/openapi3' publishes for it refers to itself with no value passed through on the way, which JSON Schema leaves undefined`, "If a member is meant to name another type, qualify it, as 'TypeSpec.string' names the scalar; if the recursion is meant, hold it in a model property or a list.");
1807
+ }
1808
+ /** TypeSpec's own assignability, which an override is checked against and a rename is not. */
1809
+ export function isAssignable(program, source, target) {
1810
+ return $(program).type.isAssignableTo(source, target);
1811
+ }
1812
+ function isNull(type) {
1813
+ return type.kind === "Intrinsic" && type.name === "null";
1814
+ }
1815
+ /** Whether a type admits `null`: it is `null`, or a union with `null` among its members. */
1816
+ function hasNull(type) {
1817
+ return membersOf(type).some(isNull);
1818
+ }
1819
+ /** The fixed values of a type that is literals and nothing else, `null` aside; else `undefined`. */
1820
+ function literalValuesOfType(type) {
1821
+ const members = membersOf(type).filter((member) => !(member.kind === "Intrinsic" && member.name === "null"));
1822
+ const values = members.map(literalValueOf);
1823
+ return values.length > 0 && values.every((value) => value !== undefined)
1824
+ ? values
1825
+ : undefined;
1826
+ }
1827
+ /**
1828
+ * Whether a declared type admits a literal value.
1829
+ *
1830
+ * **A scalar admits what the check it emits admits** - see `scalarAccepts` - not every value of its
1831
+ * JSON type: `"red"` is a string and no `utcDateTime`. Read by type alone, `@oneOf { Color, string }`
1832
+ * merged with `k: utcDateTime` kept `"red"` and `"green"` as members of their own, and a value the
1833
+ * document finds in two members was accepted as one.
1834
+ */
1835
+ function admitsValue(program, type, value) {
1836
+ switch (type.kind) {
1837
+ case "UnionVariant":
1838
+ return admitsValue(program, type.type, value);
1839
+ case "Scalar": {
1840
+ const json = jsonTypeOfScalar(program, type);
1841
+ const kind = typeof value === "number"
1842
+ ? Number.isInteger(value)
1843
+ ? "integer"
1844
+ : "fraction"
1845
+ : typeof value;
1846
+ const fits = json === undefined ||
1847
+ json === kind ||
1848
+ (json === "number" && (kind === "integer" || kind === "fraction"));
1849
+ return (fits &&
1850
+ brokenConstraintOf(program, type, value) === undefined &&
1851
+ scalarAccepts(program, type, value) !== false);
1852
+ }
1853
+ case "String":
1854
+ case "Number":
1855
+ case "Boolean":
1856
+ return type.value === value;
1857
+ case "EnumMember":
1858
+ return (type.value ?? type.name) === value;
1859
+ case "Enum":
1860
+ return membersOf(type).some((member) => admitsValue(program, member, value));
1861
+ case "Union": {
1862
+ // Under `@oneOf`, a value two members admit is no value of the union.
1863
+ const admitting = alikeMembersOf(program, type).filter((member) => admitsValue(program, member, value));
1864
+ return isExclusive(program, type) ? admitting.length === 1 : admitting.length > 0;
1865
+ }
1866
+ case "Intrinsic":
1867
+ return type.name === "unknown";
1868
+ default:
1869
+ return false;
1870
+ }
1871
+ }
1872
+ /** Whether two declarations of one key share no JSON type, so no value is both. */
1873
+ function disjoint(program, one, other) {
1874
+ const a = valueJsonTypesOf(program, one);
1875
+ const b = valueJsonTypesOf(program, other);
1876
+ if (a === undefined || b === undefined)
1877
+ return false;
1878
+ const numeric = (type) => type === "integer" || type === "number" || type === "fraction";
1879
+ return ![...a].some((x) => [...b].some((y) => x === y || (numeric(x) && numeric(y))));
1880
+ }
1881
+ /** The JSON types a property's value is published as, `@encode` included; `undefined` for any. */
1882
+ function valueJsonTypesOf(program, property) {
1883
+ const encoding = encodedValueOf(program, property);
1884
+ if (encoding.kind === "replaced" || encoding.kind === "both") {
1885
+ const json = jsonTypeOfScalar(program, encoding.scalar);
1886
+ if (json === undefined)
1887
+ return undefined;
1888
+ return new Set(encoding.nullable ? [json, "null"] : [json]);
1889
+ }
1890
+ return jsonTypesOf(program, property.type);
1891
+ }
1892
+ /**
1893
+ * The discriminators openapi3 adds to a model as a required `string`: each `@discriminator` in the
1894
+ * chain whose model's OWN payload properties lack it.
1895
+ */
1896
+ function injectedDiscriminatorsOf(program, model) {
1897
+ const names = [];
1898
+ for (let link = model; link !== undefined; link = link.baseModel) {
1899
+ const discriminator = getDiscriminator(program, link);
1900
+ if (discriminator === undefined)
1901
+ continue;
1902
+ // Read where openapi3 reads the model's properties: a read-only `kind` sent in a create body is not.
1903
+ const declared = [...link.properties.values()].some((property) => atPublication(program, property, () => isPayloadProperty(program, property)) &&
1904
+ !isNeverType(property.type) &&
1905
+ rawPropertyKey(program, property) === discriminator.propertyName);
1906
+ if (!declared)
1907
+ names.push(discriminator.propertyName);
1908
+ }
1909
+ return names;
1910
+ }
1911
+ const NOTHING = { kind: "never" };
1912
+ /** No value because a declaration is refused - see `HeldDeclarations.refusedAt`. */
1913
+ const WITHHELD = { kind: "never", withheld: true };
1914
+ /** The part a property publishes: its declared type, or its encoded one, with its keywords. */
1915
+ function partOfProperty(program, property) {
1916
+ const encoding = encodedValueOf(program, property);
1917
+ switch (encoding.kind) {
1918
+ case "unsatisfiable":
1919
+ return undefined;
1920
+ case "both":
1921
+ return { members: [], nullable: false, keywords: [], whole: property };
1922
+ case "replaced":
1923
+ return {
1924
+ members: [encoding.scalar],
1925
+ nullable: encoding.nullable,
1926
+ keywords: [property],
1927
+ type: encoding.scalar,
1928
+ };
1929
+ case "declared":
1930
+ return partOfType(program, property.type, [property], exclusiveSiteOf(program, property, property.type) !== undefined);
1931
+ }
1932
+ }
1933
+ /**
1934
+ * The part a declared type is, its members spelled out. `exclusiveHere` where the property declaring
1935
+ * it makes its inline union a `oneOf` - see `exclusiveSiteOf`: merged under a redeclaration, `@oneOf
1936
+ * u: X1 | X2` was an `anyOf`, and `{ u: {} }`, which both members match, was accepted.
1937
+ */
1938
+ function partOfType(program, type, keywords, exclusiveHere = false) {
821
1939
  /**
822
- * **A property that refers back into an unfinished declaration is emitted as a GETTER.**
823
- *
824
- * `model InnerModel { children?: InnerModel[] }` is a plain recursive tree, and openapi3 publishes
825
- * it as a self-`$ref`. Emitting `children: z.array(innerModelSchema)` reads the `const` inside its
826
- * own initialiser, so the module throws on import; deferring the reference behind a getter is the
827
- * idiom Zod documents for exactly this, and it is the only one that keeps `z.infer` inferring the
828
- * recursive type rather than collapsing to `any`. `z.lazy(() => innerModelSchema)` runs correctly
829
- * but does not typecheck - measured: `TS7022`, "implicitly has type 'any' because it does not have
830
- * a type annotation and is referenced directly or indirectly in its own initializer".
1940
+ * A `@discriminated` union is published as `oneOf` too: its tag makes the members disjoint, until
1941
+ * a PATCH makes the tag optional, and `{ value: { a: "s" } }` then matches two open envelopes.
831
1942
  */
832
- const members = declared
833
- .filter((property) => isPayloadProperty(program, property))
1943
+ const exclusive = exclusiveHere ||
1944
+ isExclusive(program, type) ||
1945
+ (type.kind === "Union" && getDiscriminatedUnion(program, type)[0] !== undefined);
1946
+ const members = [];
1947
+ /**
1948
+ * **How many members a `null` matches**, a nested union's included: kept whole, its `null` set
1949
+ * nothing, and `@oneOf { v0: float | null, v1: string }` refused the `null` it accepts. Under
1950
+ * `@oneOf` it is a value where exactly one member matches it.
1951
+ */
1952
+ let nullMatches = 0;
1953
+ let refused;
1954
+ const visit = (each) => {
1955
+ if (each.kind === "UnionVariant")
1956
+ return visit(each.type);
1957
+ if (isNull(each)) {
1958
+ nullMatches += 1;
1959
+ return;
1960
+ }
1961
+ if (each.kind === "Union") {
1962
+ // A nested union is one member, whole, unless both are `anyOf` - see `alikeMembersOf`.
1963
+ if (each !== type && (exclusive || isPublishedOneOf(program, each))) {
1964
+ if (admitsNullValue(program, each))
1965
+ nullMatches += 1;
1966
+ members.push(each);
1967
+ return;
1968
+ }
1969
+ const [discriminated] = getDiscriminatedUnion(program, each);
1970
+ if (discriminated !== undefined && discriminated.defaultVariant !== undefined) {
1971
+ refused = "it is a discriminated union with a default variant";
1972
+ }
1973
+ if (discriminated !== undefined && discriminated.options.envelope !== "none") {
1974
+ for (const [name, variant] of discriminated.variants) {
1975
+ members.push({
1976
+ kind: "Envelope",
1977
+ union: each,
1978
+ name: String(name),
1979
+ variant,
1980
+ discriminator: discriminated.options.discriminatorPropertyName,
1981
+ property: discriminated.options.envelopePropertyName,
1982
+ });
1983
+ }
1984
+ return;
1985
+ }
1986
+ for (const variant of each.variants.values())
1987
+ visit(variant.type);
1988
+ return;
1989
+ }
1990
+ if (each.kind === "Enum") {
1991
+ members.push(...each.members.values());
1992
+ return;
1993
+ }
1994
+ if (isTypelessMember(program, each))
1995
+ nullMatches += 1;
1996
+ members.push(each);
1997
+ };
1998
+ visit(type);
1999
+ return {
2000
+ members: exclusive ? withoutRepeatedLiterals(members) : members,
2001
+ nullable: exclusive ? nullMatches === 1 : nullMatches > 0,
2002
+ keywords,
2003
+ type,
2004
+ ...(refused === undefined ? {} : { refused }),
2005
+ ...(exclusive ? { exclusive: true } : {}),
2006
+ };
2007
+ }
2008
+ /** The `string` openapi3 adds as a discriminator where the model declaring it does not. */
2009
+ function stringPart(program) {
2010
+ const string = program.checker.getStdType("string");
2011
+ return { members: [string], nullable: false, keywords: [], type: string };
2012
+ }
2013
+ function isEnvelope(member) {
2014
+ return member.kind === "Envelope";
2015
+ }
2016
+ /** A part with no JSON type of its own, which asserts only its keywords on whatever the value is. */
2017
+ function isTypelessPart(program, part) {
2018
+ return (part.whole === undefined &&
2019
+ part.members.length > 0 &&
2020
+ // `@oneOf { a: unknown, b: unknown }` is two members, which every value matches: none passes.
2021
+ !(part.exclusive === true && part.members.length > 1) &&
2022
+ part.members.every((member) => !isEnvelope(member) && isTypelessMember(program, member)));
2023
+ }
2024
+ /** Merges in flight, so one that reaches itself again is refused rather than followed forever. */
2025
+ const mergesInFlight = [];
2026
+ export function mergeParts(program, given) {
2027
+ if (given.some((part) => part === undefined))
2028
+ return NOTHING;
2029
+ const parts = given;
2030
+ const refused = parts.find((part) => part.refused !== undefined)?.refused;
2031
+ if (refused !== undefined)
2032
+ return { kind: "refused", why: refused };
2033
+ const wholes = parts.filter((part) => part.whole !== undefined);
2034
+ if (wholes.length > 0) {
2035
+ const others = parts.filter((part) => part.whole === undefined);
2036
+ return {
2037
+ kind: "all",
2038
+ parts: [
2039
+ ...wholes.map((part) => ({ kind: "whole", property: part.whole })),
2040
+ ...(others.length === 0 ? [] : [mergeParts(program, others)]),
2041
+ ],
2042
+ };
2043
+ }
2044
+ const keywordsOnly = parts.filter((part) => isTypelessPart(program, part));
2045
+ const typed = parts.filter((part) => !isTypelessPart(program, part));
2046
+ const loose = keywordsOnly.flatMap((part) => part.keywords);
2047
+ if (typed.length === 0) {
834
2048
  /**
835
- * **A `never` property is DROPPED, exactly as the document drops it.** `model N { value:
836
- * never; other: string }` is published by `@typespec/openapi3` as `{other}` with
837
- * `required: ["other"]` - `value` is absent entirely, because a property that can hold no value
838
- * is not part of the payload.
839
- *
840
- * This emitter kept it and wrote `value: z.never()`, which made the model unsatisfiable: measured,
841
- * `{"other":"x"}` - the exact body the document describes - was REJECTED with "expected never,
842
- * received undefined". So the validator generated from a document rejected every request that
843
- * document calls valid, which is the divergence class this package exists to prevent.
844
- *
845
- * Dropping it makes the two artefacts agree and leaves nothing to refuse. `never` in a position
846
- * that is not a property - a body, a variant - is still refused by `typeToZodBody`, because there
847
- * it is not something the document quietly omits.
2049
+ * **A `@oneOf` of a typeless member and `null` refuses the `null` both match**, and is its own
2050
+ * union, whole: `@oneOf { a: unknown, b: null }` beside `unknown` accepted `null` as keywords.
848
2051
  */
849
- .filter((property) => !isNeverType(property.type))
850
- .map((property) => {
851
- const key = propertyKey(program, property);
852
- const { value, deferred } = captureBackEdges(() => withDeclarationSite(property, () => propertyToZod(program, property)));
2052
+ const nonNull = keywordsOnly.find((part) => !part.nullable && part.type !== undefined);
2053
+ if (nonNull !== undefined) {
2054
+ return { kind: "type", type: nonNull.type, keywords: loose };
2055
+ }
2056
+ const first = keywordsOnly[0]?.members[0] ?? $(program).intrinsic.any;
2057
+ return { kind: "type", type: first, keywords: loose };
2058
+ }
2059
+ // A typeless part admits `null` unless it is such a `@oneOf`, whose refusal stands.
2060
+ const nullable = parts.every((part) => part.nullable);
2061
+ if (typed.some((part) => part.members.length === 0)) {
2062
+ return nullable ? nullValue(program) : NOTHING;
2063
+ }
2064
+ const [first, ...rest] = typed;
2065
+ const value = mergeNonNull(program, [
2066
+ { ...first, keywords: [...first.keywords, ...loose] },
2067
+ ...rest,
2068
+ ]);
2069
+ return nullable ? orNullMerged(program, value) : value;
2070
+ }
2071
+ function nullValue(program) {
2072
+ return { kind: "type", type: $(program).intrinsic.null, keywords: [] };
2073
+ }
2074
+ function orNullMerged(program, value) {
2075
+ if (value.kind === "never")
2076
+ return nullValue(program);
2077
+ if (value.kind === "type" && hasNull(value.type))
2078
+ return value;
2079
+ return { kind: "nullable", inner: value };
2080
+ }
2081
+ /** Parts that each admit no `null`, and are each one or more members. */
2082
+ function mergeNonNull(program, parts) {
2083
+ /**
2084
+ * **A member with no type asserts only its keywords**, here as at the top of a merge: split out of
2085
+ * `@oneOf { v0: unknown, v1: string }` beside an envelope, `unknown` reached the intersection of two
2086
+ * scalars and was refused as an unknown kind, `Envelope`, where the value is the envelope.
2087
+ */
2088
+ const typeless = parts.filter((part) => isTypelessPart(program, part));
2089
+ if (typeless.length > 0 && typeless.length < parts.length) {
2090
+ const [first, ...rest] = parts.filter((part) => !isTypelessPart(program, part));
2091
+ const loose = typeless.flatMap((part) => part.keywords);
2092
+ return mergeNonNull(program, [{ ...first, keywords: [...first.keywords, ...loose] }, ...rest]);
2093
+ }
2094
+ const keywords = parts.flatMap((part) => part.keywords);
2095
+ const [only] = parts;
2096
+ if (parts.length === 1 && only !== undefined && only.members.length === 1) {
2097
+ const member = only.members[0];
2098
+ return isEnvelope(member)
2099
+ ? mergeObjects(program, [only])
2100
+ : memberValue(program, member, only.keywords, only.at);
2101
+ }
2102
+ if (disjointParts(program, parts))
2103
+ return NOTHING;
2104
+ // A part whose type every other part's admits is the value, every part's keywords on it.
2105
+ for (const candidate of parts) {
2106
+ const type = typeOfPart(candidate);
2107
+ if (type === undefined)
2108
+ continue;
2109
+ const scalars = [];
2110
+ const narrows = parts.every((other) => {
2111
+ if (other === candidate)
2112
+ return true;
2113
+ const target = typeOfPart(other);
2114
+ const each = target === undefined || !publishedAlike(program, target, candidate, other)
2115
+ ? undefined
2116
+ : narrowsTo(program, type, target);
2117
+ if (each !== undefined)
2118
+ scalars.push(...each);
2119
+ return each !== undefined;
2120
+ });
2121
+ if (narrows)
2122
+ return memberValue(program, type, [...keywords, ...scalars], candidate.at);
2123
+ }
2124
+ /**
2125
+ * **A nested union kept whole is spelled out where it meets another declaration**, its members
2126
+ * held to the rest with its own exclusivity: `C` beside `@oneOf { v0: A | B, v1: string }` is
2127
+ * `C & A` or `C & B`. Left whole, the two objects were joined with `.and()`, which pooled the keys
2128
+ * a sealed side refuses and threw on the two defaults of `k`.
2129
+ */
2130
+ const nested = parts.findIndex((part) => part.members.length === 1 &&
2131
+ !isEnvelope(part.members[0]) &&
2132
+ part.members[0].kind === "Union");
2133
+ if (nested !== -1) {
2134
+ const part = parts[nested];
2135
+ const spelled = partOfType(program, part.members[0], part.keywords);
2136
+ if (spelled.members.length === 0)
2137
+ return NOTHING;
2138
+ return mergeNonNull(program, parts.map((each, index) => index === nested ? { ...spelled, ...(part.at === undefined ? {} : { at: part.at }) } : each));
2139
+ }
2140
+ // A union is held to the rest member by member, and a member nothing else admits is dropped.
2141
+ const split = parts.findIndex((part) => part.members.length > 1);
2142
+ if (split !== -1) {
2143
+ const union = parts[split];
2144
+ const members = union.members.map((member) => mergeNonNull(program, parts.map((part, index) => index === split
2145
+ ? {
2146
+ members: [member],
2147
+ nullable: false,
2148
+ keywords: union.keywords,
2149
+ ...(union.at === undefined ? {} : { at: union.at }),
2150
+ }
2151
+ : part)));
853
2152
  /**
854
- * **A getter without a return type infers `unknown`, and the collapse is SILENT.**
855
- *
856
- * The docblock above claims the getter keeps `z.infer` inferring the recursive type. It does
857
- * for the declaration itself, and not for a schema that WRAPS it: measured on the conformance
858
- * corpus, `z.record(z.string(), innerModelSchema)` - what `@body body: Record<InnerModel>`
859
- * emits - infers `children?: unknown`, and `.parse()` on it returns `unknown` outright. The
860
- * generated server then failed to compile with `TS2322`, in `type/dictionary`, which is how
861
- * this was found; nothing said the emitted TYPE was weaker than the document.
862
- *
863
- * Annotating the GETTER rather than the declaration is what keeps `.shape` and `.extend`,
864
- * which inheritance and every group spread depend on. `: z.ZodType<T, T>` names both of Zod's
865
- * parameters for the reason `renderSchemas` names both: one leaves the input side `unknown`.
2153
+ * **A `@oneOf` holding a refused member validates nothing**: the document still counts the
2154
+ * member, so a value it and another both match is refused there, and dropped here it was
2155
+ * accepted - as a merge holding a refused declaration holds nothing.
866
2156
  */
867
- const annotation = deferred ? (annotateDeferred(property) ?? "") : "";
2157
+ const refusal = members.find(holdsRefusal);
2158
+ if (union.exclusive === true && refusal !== undefined)
2159
+ return heldRefusalOf(refusal);
2160
+ const kept = members.filter((member) => member.kind !== "never");
2161
+ const [one] = kept;
2162
+ if (one === undefined)
2163
+ return NOTHING;
2164
+ // A `@oneOf` union stays exclusive member by member: each merged member matches where its own did.
2165
+ if (kept.length === 1)
2166
+ return one;
2167
+ return union.exclusive === true
2168
+ ? { kind: "union", members: kept, exclusive: true }
2169
+ : { kind: "union", members: kept };
2170
+ }
2171
+ const members = parts.map((part) => part.members[0]);
2172
+ const fixed = members.find((member) => !isEnvelope(member) && literalValueOf(member) !== undefined);
2173
+ if (fixed !== undefined) {
2174
+ const value = literalValueOf(fixed);
2175
+ const admitted = members.every((member) => !isEnvelope(member) && admitsValue(program, member, value));
2176
+ const kept = keywords.every((source) => brokenConstraintOf(program, source, value) === undefined);
2177
+ return admitted && kept ? { kind: "type", type: fixed, keywords: [] } : NOTHING;
2178
+ }
2179
+ if (members.every((member) => isObjectMember(member)))
2180
+ return mergeObjects(program, parts);
2181
+ if (members.every((member) => !isEnvelope(member) && isListMember(member))) {
2182
+ return mergeLists(program, parts);
2183
+ }
2184
+ if (members.some((member) => !isEnvelope(member) && member.kind === "Tuple")) {
2185
+ return { kind: "refused", why: "a tuple is held to another declaration" };
2186
+ }
2187
+ // Two scalars of one JSON type that neither narrows: the value is checked against both.
2188
+ return {
2189
+ kind: "all",
2190
+ parts: parts.map((part) => memberValue(program, part.members[0], part.keywords, part.at)),
2191
+ };
2192
+ }
2193
+ /**
2194
+ * **Whether a type is one schema at the visibility each of two parts is published at**, so that
2195
+ * what narrows it at one narrows it at the other. A record's value is published at the record's
2196
+ * visibility: `QX { x: M1 }` sent in a PATCH beside a `Record<M1>` holds `x` to `M1Update` and to
2197
+ * `M1`, and taken as narrowed by itself it was `M1Update` alone, `c` optional where `M1` requires it.
2198
+ */
2199
+ function publishedAlike(program, type, one, other) {
2200
+ const a = one.at ?? visibility;
2201
+ const b = other.at ?? visibility;
2202
+ if (a !== b &&
2203
+ renderedVisibilityOf(program, type, a) !== renderedVisibilityOf(program, type, b)) {
2204
+ return false;
2205
+ }
2206
+ const own = typeOfPart(one);
2207
+ return own === undefined || !elementsPublishedApart(program, own, a, type, b);
2208
+ }
2209
+ /** The one type a part stands for, where it can be rendered whole: a single member, or its union. */
2210
+ function typeOfPart(part) {
2211
+ if (part.members.length === 1) {
2212
+ const [member] = part.members;
2213
+ return member === undefined || isEnvelope(member) ? undefined : member;
2214
+ }
2215
+ if (part.members.some(isEnvelope))
2216
+ return undefined;
2217
+ return part.type !== undefined && !hasNull(part.type) ? part.type : undefined;
2218
+ }
2219
+ /**
2220
+ * A member as the value, its keywords on it - a literal checked against them now - published at
2221
+ * `at` where its part is published apart from the merge: a record's value, at the record's.
2222
+ */
2223
+ function memberValue(program, type, keywords, at) {
2224
+ const value = literalValueOf(type);
2225
+ if (value === undefined)
2226
+ return { kind: "type", type, keywords, ...(at === undefined ? {} : { at }) };
2227
+ return keywords.every((source) => brokenConstraintOf(program, source, value) === undefined)
2228
+ ? { kind: "type", type, keywords: [] }
2229
+ : NOTHING;
2230
+ }
2231
+ /** Whether the parts share no JSON type, so no value is one of each. */
2232
+ function disjointParts(program, parts) {
2233
+ const typesOf = (part) => {
2234
+ const types = new Set();
2235
+ for (const member of part.members) {
2236
+ const each = isEnvelope(member)
2237
+ ? new Set(["object"])
2238
+ : jsonTypesOf(program, member);
2239
+ if (each === undefined)
2240
+ return undefined;
2241
+ for (const type of each)
2242
+ types.add(type);
2243
+ }
2244
+ return types;
2245
+ };
2246
+ const numeric = (type) => type === "integer" || type === "number" || type === "fraction";
2247
+ let shared;
2248
+ for (const part of parts) {
2249
+ const types = typesOf(part);
2250
+ if (types === undefined)
2251
+ continue;
2252
+ shared =
2253
+ shared === undefined
2254
+ ? [...types]
2255
+ : shared.filter((type) => [...types].some((other) => other === type || (numeric(other) && numeric(type))));
2256
+ }
2257
+ return shared !== undefined && shared.length === 0;
2258
+ }
2259
+ function isObjectMember(member) {
2260
+ if (isEnvelope(member))
2261
+ return true;
2262
+ return member.kind === "Model" && effectiveIndexer(member)?.key.name !== "integer";
2263
+ }
2264
+ function isListMember(member) {
2265
+ return member.kind === "Model" && effectiveIndexer(member)?.key.name === "integer";
2266
+ }
2267
+ /** Lists merged element by element, with every declaration's list keywords. */
2268
+ function mergeLists(program, parts) {
2269
+ const lists = parts.map((part) => part.members[0]);
2270
+ // Array literals all, the element is an `Item`'s - see `atItem`.
2271
+ const [first] = lists;
2272
+ const literal = lists.every((list) => isArrayLiteral(program, list)) ? first : undefined;
2273
+ // Each list's element at its own visibility - a literal's with `Item`, a named list's without.
2274
+ const merge = () => mergeParts(program, lists.map((list, index) => {
2275
+ const at = parts[index]?.at ?? visibility;
868
2276
  return {
869
- deferred,
870
- text: deferred
871
- ? `\tget ${key}()${annotation} {\n\t\treturn ${value};\n\t},`
872
- : `\t${key}: ${value},`,
2277
+ ...partOfType(program, effectiveIndexer(list).value, []),
2278
+ at: isArrayLiteral(program, list) ? at | Visibility.Item : at,
873
2279
  };
2280
+ }));
2281
+ const element = literal === undefined ? merge() : atItem(program, literal, merge);
2282
+ return {
2283
+ kind: "array",
2284
+ element,
2285
+ keywords: parts.flatMap((part, index) => [...part.keywords, lists[index]]),
2286
+ ...(literal === undefined ? {} : { list: literal }),
2287
+ };
2288
+ }
2289
+ /**
2290
+ * A model as the object it publishes at the current position, or `undefined` where it accepts
2291
+ * nothing - refused as `invalid-subtype-discriminator` or `sealed-base-extended`.
2292
+ */
2293
+ function shapeOfModel(program, model, keywords) {
2294
+ if (unsatisfiableSubtype(program, model))
2295
+ return undefined;
2296
+ const extension = sealedBaseExtensionOf(program, model);
2297
+ if (extension?.required === true)
2298
+ return undefined;
2299
+ const binding = bindingIndexerOf(model);
2300
+ const rest = binding !== undefined && !isNeverType(binding.value) ? binding.value : undefined;
2301
+ /**
2302
+ * The keys the binding indexer's own model and those above it publish, which it does not hold. A
2303
+ * key whose declaration openapi3 leaves out of the properties - see `atPublication` - is the
2304
+ * indexer's: `additionalProperties` evaluates it.
2305
+ */
2306
+ const exempt = binding === undefined
2307
+ ? undefined
2308
+ : new Set(publishedPropertiesOf(program, binding.link).map(({ property }) => rawPropertyKey(program, property)));
2309
+ /**
2310
+ * **A string indexer only means "dictionary" when the model declares NO properties** - read from
2311
+ * the UNFILTERED list, because a model whose only properties are metadata is still a declared
2312
+ * object rather than a dictionary.
2313
+ */
2314
+ const dictionary = rest !== undefined && flattenedPropertiesOf(program, model).length === 0;
2315
+ const discriminators = discriminatorsOf(program, model);
2316
+ const injected = injectedDiscriminatorsOf(program, model);
2317
+ const published = new Map(publishedPropertiesOf(program, model).map((entry) => [
2318
+ rawPropertyKey(program, entry.property),
2319
+ entry,
2320
+ ]));
2321
+ const members = renderedPropertiesOf(program, model)
2322
+ /**
2323
+ * An optional property a template instance adds to a base openapi3 seals is refused by the
2324
+ * document whenever it is sent - see `sealedBaseExtensionOf` - so it is not declared, and the
2325
+ * sealed object refuses it too.
2326
+ */
2327
+ .filter(({ property }) => extension === undefined || !extension.keys.includes(rawPropertyKey(program, property)))
2328
+ .flatMap(({ property, overridden }) => {
2329
+ const key = rawPropertyKey(program, property);
2330
+ const rendered = [property, ...overridden];
2331
+ const bound = binding !== undefined && exempt !== undefined && !exempt.has(key);
2332
+ // `required` is read where the model is reduced to, as openapi3 reads it.
2333
+ const required = discriminators.includes(key) ||
2334
+ rendered.some((declaration) => isRequiredAt(program, declaration));
2335
+ const entry = published.get(key);
2336
+ /**
2337
+ * **A key no published declaration states is held by what covers it in the document**: the
2338
+ * indexer, or the `string` openapi3 adds for a discriminator, below. One nothing covers is
2339
+ * `split-visibility`, refused, and keeps the declaration it was rendered from.
2340
+ */
2341
+ if (entry === undefined && required && discriminators.includes(key))
2342
+ return [];
2343
+ const declarations = entry !== undefined
2344
+ ? [entry.property, ...entry.overridden]
2345
+ : bound && rest !== undefined
2346
+ ? []
2347
+ : rendered;
2348
+ return [
2349
+ {
2350
+ key,
2351
+ declarations,
2352
+ // A key the binding indexer holds is held to its value as well - see `bindingIndexerOf`.
2353
+ beside: bound && rest !== undefined ? [partOfType(program, rest, [])] : [],
2354
+ forbidden: bound && rest === undefined,
2355
+ injected: injected.includes(key),
2356
+ required,
2357
+ },
2358
+ ];
874
2359
  });
875
- const entries = members.map((member) => member.text);
2360
+ // A key a `Record<never>` above it refuses: no value where it is required, refused where sent.
2361
+ if (members.some((member) => member.forbidden && member.required))
2362
+ return undefined;
876
2363
  /**
877
- * A model-level `@refine` goes through the same path as every other constraint, so the predicate's
878
- * import is recorded exactly once, in one place. Handling it separately here is what dropped the
879
- * import the first time - the emitted schema referenced a name it never imported, and only the
880
- * compile check in `test/emit.test.ts` noticed.
881
- *
882
- * `.refine()` returns an effect rather than an object, so `applyConstraints` appending it after
883
- * `.loose()` is also the only order that works.
2364
+ * **The discriminator openapi3 adds where nothing declares it**, a required `string`, on the base
2365
+ * and - through `allOf` - on every model under it, an intermediate subtype standing alone included.
884
2366
  */
2367
+ for (const key of discriminators) {
2368
+ if (members.some((member) => member.key === key))
2369
+ continue;
2370
+ members.push({
2371
+ key,
2372
+ declarations: [],
2373
+ beside: [stringPart(program)],
2374
+ forbidden: false,
2375
+ injected: false,
2376
+ required: true,
2377
+ });
2378
+ }
2379
+ return {
2380
+ members: members.filter((member) => !member.forbidden),
2381
+ sealed: rest === undefined && (binding !== undefined || isSealed(model)),
2382
+ rest,
2383
+ dictionary,
2384
+ keywords: [...keywords, model],
2385
+ site: model,
2386
+ at: visibility,
2387
+ };
2388
+ }
2389
+ /**
2390
+ * **The indexer that holds a model's keys: the TOPMOST string indexer in its chain.**
2391
+ *
2392
+ * openapi3 publishes a root model's indexer as `additionalProperties`, which holds every key the
2393
+ * model does not declare itself - a subtype's own keys included, since the subtype is `allOf` the
2394
+ * root - and a lower model's as `unevaluatedProperties`, which the root's has already evaluated. So
2395
+ * the topmost indexer holds every key its own model and those above it do not declare, and any below
2396
+ * it never applies. Read as the nearest, a subtype's `m: Inner2` escaped the base's
2397
+ * `...Record<Inner>` and a subtype's own indexer refused keys the document accepts.
2398
+ */
2399
+ export function bindingIndexerOf(model) {
2400
+ let binding;
2401
+ for (let link = model; link !== undefined; link = link.baseModel) {
2402
+ if (link.indexer?.key.name === "string")
2403
+ binding = { link, value: link.indexer.value };
2404
+ }
2405
+ return binding;
2406
+ }
2407
+ /** The wrapper openapi3 publishes for an envelope, as an object of its two required members. */
2408
+ function shapeOfEnvelope(program, envelope) {
2409
+ const tag = $(program).literal.createString(envelope.name);
2410
+ // Published where the union reduces to - see `envelopeVisibilityOf` - not where a merge spells it out.
2411
+ const at = envelopeVisibilityOf(program, envelope.union, envelope.variant);
2412
+ // Both optional where a PATCH makes them so, as the envelope is published.
2413
+ const required = !implicitlyOptional(at);
2414
+ return {
2415
+ members: [
2416
+ {
2417
+ key: envelope.discriminator,
2418
+ declarations: [],
2419
+ beside: [{ members: [tag], nullable: false, keywords: [], type: tag }],
2420
+ injected: false,
2421
+ required,
2422
+ },
2423
+ {
2424
+ key: envelope.property,
2425
+ declarations: [],
2426
+ // At the envelope's visibility: a list literal's at `Item`, beside a named list's without.
2427
+ beside: [{ ...partOfType(program, envelope.variant, []), at }],
2428
+ injected: false,
2429
+ required,
2430
+ },
2431
+ ],
2432
+ sealed: sealObjectSchemas,
2433
+ rest: undefined,
2434
+ dictionary: false,
2435
+ keywords: [],
2436
+ site: envelope.union,
2437
+ at,
2438
+ };
2439
+ }
2440
+ /** Objects merged key by key - see {@link Merged}. */
2441
+ function mergeObjects(program, parts) {
2442
+ const members = parts.map((part) => part.members[0]);
2443
+ if (mergesInFlight.some((entry) => entry.length === members.length && entry.every((member) => members.includes(member)))) {
2444
+ return { kind: "refused", why: "the merged value refers back to itself", recursive: true };
2445
+ }
2446
+ mergesInFlight.push(members);
2447
+ try {
2448
+ const shapes = [];
2449
+ for (const [index, member] of members.entries()) {
2450
+ const part = parts[index];
2451
+ const requested = part.at ?? visibility;
2452
+ const at = isEnvelope(member) ? requested : renderedVisibilityOf(program, member, requested);
2453
+ /**
2454
+ * A component openapi3 publishes split - refused as `split-visibility` wherever it is
2455
+ * rendered - holds a value to a schema no value satisfies, and so does every merge holding it.
2456
+ * Merged from its declared shape, `Sub` accepted what its `allOf` refuses.
2457
+ */
2458
+ if (!isEnvelope(member) && isPublishedSplit(program, member, at))
2459
+ return WITHHELD;
2460
+ if (!isEnvelope(member) && held.refusedAt(member, at))
2461
+ return WITHHELD;
2462
+ const shape = withVisibility(at, () => isEnvelope(member)
2463
+ ? shapeOfEnvelope(program, member)
2464
+ : shapeOfModel(program, member, part.keywords));
2465
+ if (shape === undefined)
2466
+ return NOTHING;
2467
+ shapes.push(shape);
2468
+ }
2469
+ return mergeShapes(program, shapes);
2470
+ }
2471
+ finally {
2472
+ mergesInFlight.pop();
2473
+ }
2474
+ }
2475
+ function mergeShapes(program, shapes) {
2476
+ const keys = [...new Set(shapes.flatMap((shape) => shape.members.map((member) => member.key)))];
885
2477
  /**
886
- * **A deferred property forces the CONSTRUCTOR form of the object.**
887
- *
888
- * Measured on Zod 4.4.3: `.strict()`, `.loose()` and `.catchall()` all read `shape` eagerly, so
889
- * each one fires the getter during module initialisation and throws `Cannot access 'X' before
890
- * initialization` - the very thing the getter exists to avoid. `z.strictObject` and
891
- * `z.looseObject` take the same shape without reading it.
2478
+ * Each declaration at the visibility its shape is published at - see `positionOfDeclarations` -
2479
+ * wherever that is not where the merge is rendered: `M13[]` beside a named list under a PATCH holds
2480
+ * its element at `Item`, where `M13` is canonical, and its `a: M14` rendered at the PATCH was
2481
+ * `M14Update`, `d` optional where the document's `M14` requires it.
2482
+ */
2483
+ const apart = shapes.some((shape) => shape.at !== visibility);
2484
+ const members = [];
2485
+ for (const key of keys) {
2486
+ const present = [];
2487
+ const declaredAt = [];
2488
+ const beside = [];
2489
+ let forbidden = false;
2490
+ for (const shape of shapes) {
2491
+ const member = shape.members.find((each) => each.key === key);
2492
+ if (member !== undefined) {
2493
+ present.push(member);
2494
+ declaredAt.push(...member.declarations.map(() => shape.at));
2495
+ }
2496
+ else if (shape.sealed)
2497
+ forbidden = true;
2498
+ else if (shape.rest !== undefined) {
2499
+ beside.push({ ...partOfType(program, shape.rest, []), at: shape.at });
2500
+ }
2501
+ }
2502
+ const required = present.some((member) => member.required);
2503
+ // A key a sealed side does not declare: refused when sent, and so no value where it is required.
2504
+ if (forbidden) {
2505
+ if (required)
2506
+ return NOTHING;
2507
+ continue;
2508
+ }
2509
+ const declarations = present.flatMap((member) => member.declarations);
2510
+ const stated = present.flatMap((member) => member.beside);
2511
+ const injected = present.some((member) => member.injected);
2512
+ const [own, ...ancestors] = declarations;
2513
+ if (own === undefined) {
2514
+ members.push({
2515
+ key,
2516
+ property: undefined,
2517
+ position: { ...OWN_POSITION, required, value: mergeParts(program, [...stated, ...beside]) },
2518
+ });
2519
+ continue;
2520
+ }
2521
+ members.push({
2522
+ key,
2523
+ property: own,
2524
+ position: positionOfDeclarations(program, own, ancestors, {
2525
+ required,
2526
+ injected,
2527
+ beside: [...stated, ...beside],
2528
+ ...(apart ? { at: declaredAt } : {}),
2529
+ }),
2530
+ });
2531
+ }
2532
+ return {
2533
+ kind: "object",
2534
+ members,
2535
+ rest: mergedRest(program, shapes),
2536
+ dictionary: shapes.every((shape) => shape.dictionary),
2537
+ keywords: shapes.flatMap((shape) => shape.keywords),
2538
+ site: shapes.length === 1 ? shapes[0]?.site : undefined,
2539
+ };
2540
+ }
2541
+ /** What a key no member names may hold, across every shape merged. */
2542
+ function mergedRest(program, shapes) {
2543
+ if (shapes.some((shape) => shape.sealed))
2544
+ return "sealed";
2545
+ const typed = shapes.filter((shape) => shape.rest !== undefined);
2546
+ if (typed.length === 0)
2547
+ return "open";
2548
+ // `...Record<unknown>` keeps what it is sent; a typed indexer, or a typeless scalar, is a schema.
2549
+ if (typed.every((shape) => isUnknownType(shape.rest)))
2550
+ return "loose";
2551
+ return mergeParts(program, typed.map((shape) => ({ ...partOfType(program, shape.rest, []), at: shape.at })));
2552
+ }
2553
+ /**
2554
+ * **Whether a merged value refuses anything a declaration states**, at any depth: a refused merge, a
2555
+ * key whose declarations cannot be merged, or a refused declaration it holds. Such a value is not
2556
+ * what the document publishes, so a `@oneOf` cannot count a value against it as the document does.
2557
+ */
2558
+ function holdsRefusal(value) {
2559
+ switch (value.kind) {
2560
+ case "never":
2561
+ return value.withheld === true;
2562
+ case "refused":
2563
+ return true;
2564
+ case "object":
2565
+ return value.members.some((member) => member.position.refused !== undefined ||
2566
+ (member.position.value !== undefined && holdsRefusal(member.position.value)));
2567
+ case "nullable":
2568
+ return holdsRefusal(value.inner);
2569
+ case "array":
2570
+ return holdsRefusal(value.element);
2571
+ case "union":
2572
+ return value.members.some(holdsRefusal);
2573
+ case "all":
2574
+ return value.parts.some(holdsRefusal);
2575
+ default:
2576
+ return false;
2577
+ }
2578
+ }
2579
+ /**
2580
+ * **The refusal a value holds, as the value's own**: the first merge it refuses, at any depth, so the
2581
+ * declaration holding the value reports why - `LeafB.u`, where `D6` meets `@oneOf { M3, M5 }` and
2582
+ * a tuple meets a list. A refused declaration it holds has reported itself, and leaves no value.
2583
+ */
2584
+ function heldRefusalOf(value) {
2585
+ switch (value.kind) {
2586
+ case "refused":
2587
+ return value;
2588
+ case "object":
2589
+ for (const member of value.members) {
2590
+ const refused = member.position.refused;
2591
+ if (refused !== undefined) {
2592
+ return {
2593
+ kind: "refused",
2594
+ why: refused.why,
2595
+ ...(refused.recursive ? { recursive: true } : {}),
2596
+ };
2597
+ }
2598
+ const inner = member.position.value;
2599
+ if (inner !== undefined && holdsRefusal(inner))
2600
+ return heldRefusalOf(inner);
2601
+ }
2602
+ return WITHHELD;
2603
+ case "nullable":
2604
+ return heldRefusalOf(value.inner);
2605
+ case "array":
2606
+ return heldRefusalOf(value.element);
2607
+ case "union": {
2608
+ const held = value.members.find(holdsRefusal);
2609
+ return held === undefined ? WITHHELD : heldRefusalOf(held);
2610
+ }
2611
+ case "all": {
2612
+ const held = value.parts.find(holdsRefusal);
2613
+ return held === undefined ? WITHHELD : heldRefusalOf(held);
2614
+ }
2615
+ default:
2616
+ return WITHHELD;
2617
+ }
2618
+ }
2619
+ /**
2620
+ * The first refusal a merged value holds outside an object's declared members, which report their
2621
+ * own. A member no declaration states - an envelope's `value` - has no one to report it, so its
2622
+ * refusal is the enclosing value's; left there, a recursive merge through an envelope was emitted as
2623
+ * `z.never()` with nothing said.
2624
+ */
2625
+ function refusalIn(value) {
2626
+ switch (value.kind) {
2627
+ case "object":
2628
+ return value.members
2629
+ .filter((member) => member.property === undefined)
2630
+ .map((member) => member.position.value === undefined ? undefined : refusalIn(member.position.value))
2631
+ .find((refusal) => refusal !== undefined);
2632
+ case "refused":
2633
+ return value;
2634
+ case "nullable":
2635
+ return refusalIn(value.inner);
2636
+ case "array":
2637
+ return refusalIn(value.element);
2638
+ case "union":
2639
+ return value.members.map(refusalIn).find((why) => why !== undefined);
2640
+ case "all":
2641
+ return value.parts.map(refusalIn).find((why) => why !== undefined);
2642
+ default:
2643
+ return undefined;
2644
+ }
2645
+ }
2646
+ /** The indexer in force, which a model can inherit - `model X extends Record<unknown>`. */
2647
+ export function effectiveIndexer(model) {
2648
+ for (let link = model; link !== undefined; link = link.baseModel) {
2649
+ if (link.indexer !== undefined)
2650
+ return link.indexer;
2651
+ }
2652
+ return undefined;
2653
+ }
2654
+ /**
2655
+ * The subtypes a `@discriminator` base dispatches to: the models its document's `mapping` names.
2656
+ *
2657
+ * **The compiler's own reading, which is what openapi3 writes the mapping from.** A derived model
2658
+ * that declares no value for the discriminator is looked through to the models derived from it
2659
+ * that do. This used to be the base's DIRECT subtypes, so `Animal` -> `Mammal` (no `kind`) -> `Cat`
2660
+ * dispatched to `Mammal`, which carries no literal `kind`, and Zod threw "Invalid discriminated union
2661
+ * option" on every parse - a known kind, an unknown one, anything - while `Cat`, the model the
2662
+ * mapping names for `"cat"`, was not in the dispatch at all.
2663
+ *
2664
+ * Filtered by openapi3's own {@link includeDerivedModel} so a template declaration is not mistaken
2665
+ * for a real subtype - the same rule that decides whether a base may be sealed.
2666
+ */
2667
+ export function discriminatedSubtypes(program, model) {
2668
+ const discriminator = getDiscriminator(program, model);
2669
+ if (discriminator === undefined)
2670
+ return [];
2671
+ return mappedSubtypes(program, model).filter((derived) => extendsModel(derived, model) &&
2672
+ invalidSubtypeOf(program, derived, discriminator.propertyName) === undefined &&
2673
+ // Refused as `split-visibility`, it is `z.never()`, which no dispatch can hold.
2674
+ !isPublishedSplit(program, derived, renderedVisibilityOf(program, derived, visibility)));
2675
+ }
2676
+ /**
2677
+ * **Whether a subtype is published as `allOf` the base at all.** `Create<Pet>` is a model of its
2678
+ * own, its mapping the canonical `Cat`'s, which holds `x` to `B` where `CreatePet` holds it to
2679
+ * `CreateB`: the document validates a value by `CreatePet`'s own properties, and dispatched to `Cat`
2680
+ * it accepted the `B` that `CreatePet` refuses.
2681
+ */
2682
+ function extendsModel(derived, model) {
2683
+ for (let link = derived.baseModel; link !== undefined; link = link.baseModel) {
2684
+ if (link === model)
2685
+ return true;
2686
+ }
2687
+ return false;
2688
+ }
2689
+ /**
2690
+ * The subtypes a discriminated base dispatches to, each with the discriminator value its mapping
2691
+ * names it for - see {@link discriminatedSubtypes}.
2692
+ */
2693
+ export function dispatchedSubtypesOf(program, model) {
2694
+ const discriminator = getDiscriminator(program, model);
2695
+ if (discriminator === undefined)
2696
+ return [];
2697
+ const held = new Set(discriminatedSubtypes(program, model));
2698
+ const { variants } = ignoreDiagnostics(getDiscriminatedUnionFromInheritance(model, discriminator));
2699
+ return [...variants].filter(([, derived]) => held.has(derived));
2700
+ }
2701
+ /** Every model the base's `mapping` names, whether or not a dispatch can hold it. */
2702
+ function mappedSubtypes(program, model) {
2703
+ const discriminator = getDiscriminator(program, model);
2704
+ if (discriminator === undefined)
2705
+ return [];
2706
+ const { variants } = ignoreDiagnostics(getDiscriminatedUnionFromInheritance(model, discriminator));
2707
+ return [...new Set(variants.values())].filter((derived) => includeDerivedModel(derived));
2708
+ }
2709
+ /**
2710
+ * **Why a model the mapping names cannot be dispatched to, or `undefined` where it can.**
2711
+ *
2712
+ * A dispatch reads one fixed value per subtype - `z.discriminatedUnion` has no other way to choose -
2713
+ * and the document's `mapping` names a subtype for exactly the values its discriminator fixes. Two
2714
+ * declarations break that, and each used to break the emitted module:
2715
+ *
2716
+ * - **a discriminator that is not one fixed value** - `kind: "lab" | string`, or a subtype that
2717
+ * renames `kind` with `@encodedName` so the `kind` it publishes is its base's open `string`. Zod
2718
+ * threw "Invalid discriminated union option" on the first parse, or on import.
2719
+ * - **a value a declaration above it refuses** - `kind: "cobra"` under `@maxLength(3) kind: string`.
2720
+ * The document publishes the subtype as `allOf` that base, so no value is one; validated as its own
2721
+ * declaration alone, every `"cobra"` was accepted.
2722
+ *
2723
+ * Each is refused as `invalid-subtype-discriminator`, as `undeclared-discriminator` refuses a
2724
+ * discriminator the document cannot state, and the base dispatches to its other subtypes.
2725
+ */
2726
+ export function invalidSubtypeOf(program, subtype, propertyName) {
2727
+ const entry = publishedPropertiesOf(program, subtype).find(({ property }) => rawPropertyKey(program, property) === propertyName);
2728
+ const values = entry === undefined ? undefined : fixedValuesOf(entry.property.type);
2729
+ /**
2730
+ * **Fixed, but not where openapi3 reads it**: `Cat { @visibility(Lifecycle.Read) kind: "cat" }`
2731
+ * first reached from a create body is published without its `kind`, which `Pet`'s `string` holds
2732
+ * in its place - see `atPublication`. Told "it publishes 'kind' as 'string'", the author was not
2733
+ * told why their `"cat"` was not it.
2734
+ */
2735
+ const hidden = values !== undefined
2736
+ ? undefined
2737
+ : flattenedPropertiesOf(program, subtype)
2738
+ .filter(({ property }) => rawPropertyKey(program, property) === propertyName)
2739
+ .flatMap(({ property, overridden }) => [property, ...overridden])
2740
+ .find((declaration) => fixedValuesOf(declaration.type) !== undefined &&
2741
+ !atPublication(program, declaration, () => isPayloadProperty(program, declaration)));
2742
+ if (hidden !== undefined) {
2743
+ const at = atPublication(program, hidden, () => visibility);
2744
+ const instead = entry === undefined
2745
+ ? "the 'string' openapi3 adds"
2746
+ : `'${entry.property.model?.name ?? "its base"}''s '${getTypeName(entry.property.type)}'`;
2747
+ return {
2748
+ why: `its '${propertyName}' is not visible at '${visibilityName(at)}', where openapi3 reads the properties of '${hidden.model?.name ?? subtype.name}', so it publishes ${instead} in its place, which is not one fixed value`,
2749
+ satisfiable: true,
2750
+ invisibleAt: at,
2751
+ };
2752
+ }
2753
+ /**
2754
+ * **Declared, but not where the request is**: `@visibility(Lifecycle.Read) kind: "dog"` is no
2755
+ * part of the schema at `Create`, which publishes the `string` openapi3 adds in its place. Told as
2756
+ * "declare it as one fixed value", the author was sent to the declaration already there.
2757
+ */
2758
+ if (entry === undefined && declaresKey(program, subtype, propertyName)) {
2759
+ return {
2760
+ why: `its '${propertyName}' is not visible at '${visibilityName(visibility)}', where it publishes the 'string' openapi3 adds in its place, which is not one fixed value`,
2761
+ satisfiable: true,
2762
+ invisibleAt: visibility,
2763
+ };
2764
+ }
2765
+ if (entry === undefined || values === undefined) {
2766
+ // Renamed away, the key it publishes is the `string` openapi3 adds where no model declares it.
2767
+ const declared = entry !== undefined
2768
+ ? `'${getTypeName(entry.property.type)}'`
2769
+ : injectedDiscriminatorsOf(program, subtype).includes(propertyName)
2770
+ ? "the 'string' openapi3 adds where no model above declares it"
2771
+ : "nothing";
2772
+ return {
2773
+ why: `it publishes '${propertyName}' as ${declared}, which is not one fixed value`,
2774
+ satisfiable: true,
2775
+ };
2776
+ }
2777
+ for (const ancestor of entry.overridden) {
2778
+ const owner = ancestor.model?.name ?? "its base";
2779
+ const published = publishedJsonTypeOf(program, ancestor);
2780
+ if (published !== undefined && published !== "string") {
2781
+ return {
2782
+ why: `it fixes '${propertyName}' as a string, which '${owner}' publishes as a JSON ${published}, so no value is one`,
2783
+ satisfiable: false,
2784
+ };
2785
+ }
2786
+ for (const value of values) {
2787
+ const broken = brokenConstraintOf(program, ancestor, value) ??
2788
+ refusedValueOf(program, ancestor.type, value);
2789
+ if (broken !== undefined) {
2790
+ return {
2791
+ why: `it fixes '${propertyName}' as '${value}', which '${owner}' refuses with ${broken}, so no value is one`,
2792
+ satisfiable: false,
2793
+ };
2794
+ }
2795
+ }
2796
+ }
2797
+ return undefined;
2798
+ }
2799
+ /** Whether a model or one above it declares a key, visible or not. */
2800
+ function declaresKey(program, model, key) {
2801
+ for (let link = model; link !== undefined; link = link.baseModel) {
2802
+ for (const property of link.properties.values()) {
2803
+ if (rawPropertyKey(program, property) === key)
2804
+ return true;
2805
+ }
2806
+ }
2807
+ return false;
2808
+ }
2809
+ /** A visibility as its flags read: `Update | Patch`, `Create | Item`. */
2810
+ export function visibilityName(at) {
2811
+ const flags = [
2812
+ ["Read", Visibility.Read],
2813
+ ["Create", Visibility.Create],
2814
+ ["Update", Visibility.Update],
2815
+ ["Delete", Visibility.Delete],
2816
+ ["Query", Visibility.Query],
2817
+ ["Patch", Visibility.Patch],
2818
+ ["Item", Visibility.Item],
2819
+ ];
2820
+ return flags
2821
+ .filter(([, flag]) => (at & flag) !== 0)
2822
+ .map(([named]) => named)
2823
+ .join(" | ");
2824
+ }
2825
+ /**
2826
+ * Why a declared type refuses a string value, read without running anything - or `undefined` where
2827
+ * it admits it. A union admits what any member admits, so `kind: K2 | null`, with `K2` a scalar whose
2828
+ * base declares `@maxLength(3)`, refuses `"cobra"` through `K2`'s chain.
2829
+ */
2830
+ function refusedValueOf(program, type, value) {
2831
+ switch (type.kind) {
2832
+ case "Scalar": {
2833
+ const json = jsonTypeOfScalar(program, type);
2834
+ if (json !== undefined && json !== "string")
2835
+ return `its type, a JSON ${json}`;
2836
+ return brokenConstraintOf(program, type, value);
2837
+ }
2838
+ case "String":
2839
+ return type.value === value ? undefined : `its value '${type.value}'`;
2840
+ case "EnumMember":
2841
+ return (type.value ?? type.name) === value ? undefined : `its value`;
2842
+ case "Enum":
2843
+ case "Union": {
2844
+ const reasons = membersOf(type).map((member) => refusedValueOf(program, member, value));
2845
+ return reasons.includes(undefined) ? undefined : `'${getTypeName(type)}'`;
2846
+ }
2847
+ case "Intrinsic":
2848
+ return type.name === "unknown" ? undefined : `'${type.name}'`;
2849
+ case "Model":
2850
+ case "Tuple":
2851
+ case "Number":
2852
+ case "Boolean":
2853
+ return `'${getTypeName(type)}'`;
2854
+ default:
2855
+ return undefined;
2856
+ }
2857
+ }
2858
+ /**
2859
+ * **Whether a model is a subtype its base's mapping names that no value can be** - its fixed
2860
+ * discriminator refused by a declaration above it. Refused as `invalid-subtype-discriminator` and
2861
+ * emitted as nothing, so used directly as a body it accepts nothing, as the document does, where it
2862
+ * used to accept its own declaration.
2863
+ */
2864
+ export function unsatisfiableSubtype(program, model) {
2865
+ for (let base = model.baseModel; base !== undefined; base = base.baseModel) {
2866
+ const discriminator = getDiscriminator(program, base);
2867
+ if (discriminator === undefined || !mappedSubtypes(program, base).includes(model))
2868
+ continue;
2869
+ if (invalidSubtypeOf(program, model, discriminator.propertyName)?.satisfiable === false) {
2870
+ return true;
2871
+ }
2872
+ }
2873
+ return false;
2874
+ }
2875
+ /** The JSON type a scalar-valued property is published as, `@encode` included. */
2876
+ function publishedJsonTypeOf(program, property) {
2877
+ const encoding = encodedValueOf(program, property);
2878
+ if (encoding.kind === "replaced" || encoding.kind === "both") {
2879
+ return jsonTypeOfScalar(program, encoding.scalar);
2880
+ }
2881
+ const type = nullableInnerOf(property.type) ?? property.type;
2882
+ return type.kind === "Scalar" ? jsonTypeOfScalar(program, type) : undefined;
2883
+ }
2884
+ /** The fixed string values a discriminator's type allows, or `undefined` where it is open. */
2885
+ function fixedValuesOf(type) {
2886
+ switch (type.kind) {
2887
+ case "String":
2888
+ return [type.value];
2889
+ case "EnumMember":
2890
+ return typeof type.value === "number" ? undefined : [type.value ?? type.name];
2891
+ case "UnionVariant":
2892
+ return fixedValuesOf(type.type);
2893
+ case "Enum":
2894
+ case "Union": {
2895
+ const values = [];
2896
+ for (const member of membersOf(type)) {
2897
+ const fixed = fixedValuesOf(member);
2898
+ if (fixed === undefined)
2899
+ return undefined;
2900
+ values.push(...fixed);
2901
+ }
2902
+ return values;
2903
+ }
2904
+ default:
2905
+ return undefined;
2906
+ }
2907
+ }
2908
+ /** `invalid-subtype-discriminator` for every model a base's mapping names that cannot be dispatched to. */
2909
+ function reportInvalidSubtypes(program, model) {
2910
+ const discriminator = getDiscriminator(program, model);
2911
+ if (discriminator === undefined)
2912
+ return;
2913
+ for (const subtype of mappedSubtypes(program, model)) {
2914
+ const invalid = invalidSubtypeOf(program, subtype, discriminator.propertyName);
2915
+ if (invalid === undefined ||
2916
+ !firstReportOf(program, "invalid-subtype-discriminator", subtype)) {
2917
+ continue;
2918
+ }
2919
+ reportDiagnostic(program, {
2920
+ code: "invalid-subtype-discriminator",
2921
+ messageId: invalid.invisibleAt === undefined ? "default" : "invisible",
2922
+ target: subtype,
2923
+ format: {
2924
+ name: subtype.name,
2925
+ base: model.name,
2926
+ property: discriminator.propertyName,
2927
+ why: invalid.why,
2928
+ at: visibilityName(invalid.invisibleAt ?? visibility),
2929
+ },
2930
+ });
2931
+ }
2932
+ }
2933
+ /**
2934
+ * **A model extending a base that openapi3 seals because it does not count the model as derived.**
2935
+ *
2936
+ * openapi3 seals a model with no derived models it publishes, and a template instance is not one
2937
+ * (`includeDerivedModel`). So with `seal-object-schemas` on, `Pet<string> extends Animal` leaves
2938
+ * `Animal` sealed, and `Pet<string>` - published as `allOf` that sealed `Animal` - refuses every
2939
+ * value carrying a property `Pet<string>` adds, since `Animal`'s own `unevaluatedProperties` cannot
2940
+ * see it. Validated as its own flattened properties, such a value was accepted. Returns the sealed
2941
+ * base and the keys it refuses, or `undefined` where nothing is refused.
2942
+ */
2943
+ export function sealedBaseExtensionOf(program, model) {
2944
+ if (includeDerivedModel(model))
2945
+ return undefined;
2946
+ for (let base = model.baseModel; base !== undefined; base = base.baseModel) {
2947
+ if (!isSealed(base))
2948
+ continue;
2949
+ const sealed = new Set(publishedPropertiesOf(program, base).map(({ property }) => rawPropertyKey(program, property)));
2950
+ const added = publishedPropertiesOf(program, model)
2951
+ .map(({ property }) => property)
2952
+ .filter((property) => !sealed.has(rawPropertyKey(program, property)));
2953
+ if (added.length === 0)
2954
+ return undefined;
2955
+ return {
2956
+ base,
2957
+ keys: added.map((property) => rawPropertyKey(program, property)),
2958
+ // Only a REQUIRED addition leaves no value; an optional one the document refuses only when sent.
2959
+ required: added.some((property) => !(property.optional || isOptionalAt(program, property))),
2960
+ };
2961
+ }
2962
+ return undefined;
2963
+ }
2964
+ export function discriminatedBaseOf(program, model) {
2965
+ const discriminator = getDiscriminator(program, model);
2966
+ const subtypes = discriminatedSubtypes(program, model);
2967
+ if (discriminator === undefined || subtypes.length === 0)
2968
+ return undefined;
2969
+ const beside = subtypes.filter((subtype) => discriminatedBaseOf(program, subtype) !== undefined);
2970
+ return {
2971
+ propertyName: discriminator.propertyName,
2972
+ dispatched: subtypes.filter((subtype) => !beside.includes(subtype)),
2973
+ beside,
2974
+ };
2975
+ }
2976
+ const NOTHING_HELD = {
2977
+ refused: () => false,
2978
+ annotated: () => false,
2979
+ refusedAt: () => false,
2980
+ holdsRefusalAt: () => false,
2981
+ };
2982
+ let held = NOTHING_HELD;
2983
+ export function withHeldDeclarations(given, run) {
2984
+ const previous = held;
2985
+ held = given;
2986
+ try {
2987
+ return run();
2988
+ }
2989
+ finally {
2990
+ held = previous;
2991
+ }
2992
+ }
2993
+ function modelToZod(program, model) {
2994
+ /**
2995
+ * **A `@discriminator` base is its own object, deserialized as a subtype where the value is one.**
2996
+ * See {@link DiscriminatedBase}.
892
2997
  *
893
- * **So the constructor form is used wherever it exists, not only where a cycle forces it.** It is
894
- * also what Zod 4 asks for: its own types say "Consider `z.looseObject(A.shape)` instead" on
895
- * `.loose()` and `.strict()`, and `.passthrough()` is deprecated outright. Emitting one spelling
896
- * means a model that gains a back edge later does not silently change form, and the eager-`shape`
897
- * hazard above stops being something this walk has to keep reasoning about.
2998
+ * `z.discriminatedUnion` is the dispatch, and composes both ways this corpus needs. Measured on
2999
+ * Zod 4.4.3: a union may be an OPTION of another union, and an option carrying a getter that refers
3000
+ * back to the union being defined does **not** throw, because the discriminator is read without
3001
+ * enumerating the rest of the shape. `Eagle.friends?: Bird[]` is exactly that. A nested base is a
3002
+ * `z.union` itself, with no literal values to dispatch on, so it sits beside the dispatch.
898
3003
  *
899
- * **A TYPED catchall has no constructor form, and that used to be a refusal.** There is nowhere
900
- * safe to put the getter - `.catchall()` reads `shape` eagerly - so the cycle was reported as
901
- * `circular-model`. `z.lazy()` is the place to put it: wrapping the whole expression defers the
902
- * getter past module initialisation, and by the time the schema is forced every declaration exists.
903
- * Measured on 4.4.3: `z.lazy(() => z.object({ get kid() {...} }).catchall(z.number()))` parses a
904
- * nested value and still applies the catchall.
3004
+ * The base arm comes last: `z.union` hands back the first arm that accepts, so a valid subtype is
3005
+ * deserialized as that subtype, and only a value no subtype accepts is the base.
905
3006
  */
906
- const lazily = members.some((member) => member.deferred);
907
- const constructible = suffix === "" || suffix === ".loose()";
908
- const shape = entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
909
- const body = constructible
910
- ? `${suffix === ".loose()" ? "z.looseObject" : strict === ".strict()" ? "z.strictObject" : "z.object"}(${shape})`
911
- : lazily
912
- ? `z.lazy(() => z.object(${shape})${suffix}${strict})`
913
- : `z.object(${shape})${suffix}${strict}`;
914
- return applyConstraints(program, body, model);
3007
+ reportInvalidSubtypes(program, model);
3008
+ const base = discriminatedBaseOf(program, model);
3009
+ if (base !== undefined) {
3010
+ const arms = [];
3011
+ const options = base.dispatched
3012
+ .map((derived) => typeToZod(program, derived))
3013
+ .filter((option) => !held.refused(option));
3014
+ const dispatched = options.filter((option) => !held.annotated(option));
3015
+ if (dispatched.length > 0) {
3016
+ arms.push(`z.discriminatedUnion(${literal(base.propertyName)}, [${dispatched.join(", ")}])`);
3017
+ }
3018
+ arms.push(...options.filter((option) => held.annotated(option)));
3019
+ arms.push(...base.beside.map((derived) => typeToZod(program, derived)));
3020
+ arms.push(modelObjectToZod(program, model));
3021
+ return `z.union([${arms.join(", ")}])`;
3022
+ }
3023
+ return modelObjectToZod(program, model);
3024
+ }
3025
+ /**
3026
+ * A model as the object of its own properties, inherited ones included.
3027
+ *
3028
+ * **Given a discriminated base, this is its base arm**: the same object, with the required `string`
3029
+ * openapi3 adds to the base's properties where the base declares no discriminator of its own.
3030
+ */
3031
+ function modelObjectToZod(program, model) {
3032
+ // Refused as `invalid-subtype-discriminator`, by the base whose mapping names it.
3033
+ if (unsatisfiableSubtype(program, model))
3034
+ return UNREPRESENTABLE;
3035
+ if (reportSealedBaseExtended(program, model))
3036
+ return UNREPRESENTABLE;
3037
+ const indexer = effectiveIndexer(model);
3038
+ if (indexer?.key.name === "integer") {
3039
+ const element = atItem(program, model, () => typeToZod(program, indexer.value));
3040
+ return applyConstraints(program, `z.array(${element})`, model);
3041
+ }
3042
+ return mergedToZod(program, objectOfModel(program, model));
3043
+ }
3044
+ /** `sealed-base-extended` for a model a sealed base leaves no value of, and whether it was one. */
3045
+ function reportSealedBaseExtended(program, model) {
3046
+ const extension = sealedBaseExtensionOf(program, model);
3047
+ if (extension?.required !== true)
3048
+ return false;
3049
+ const name = getTypeName(model, { namespaceFilter: () => false });
3050
+ // Keyed on the instance as well: `Twice<string>` and `Twice<int32>` are one node, two findings.
3051
+ if (firstReportOf(program, "sealed-base-extended", model, name)) {
3052
+ reportDiagnostic(program, {
3053
+ code: "sealed-base-extended",
3054
+ target: model,
3055
+ format: {
3056
+ name,
3057
+ base: getTypeName(extension.base, { namespaceFilter: () => false }),
3058
+ keys: extension.keys.map((key) => `'${key}'`).join(", "),
3059
+ },
3060
+ });
3061
+ }
3062
+ return true;
915
3063
  }
916
3064
  /**
917
- * **What JSON kind a value of this type arrives as, for a parameter carried in text.**
3065
+ * A model as the object it publishes at the current position: the merge of one - see
3066
+ * {@link mergeShapes}. **HTTP metadata is not payload**, and neither is a property invisible here or
3067
+ * a `never` one, which the document drops; each is judged declaration by declaration - see
3068
+ * {@link publishedPropertiesOf}.
3069
+ */
3070
+ export function objectOfModel(program, model) {
3071
+ const shape = shapeOfModel(program, model, []);
3072
+ return shape === undefined ? NOTHING : mergeShapes(program, [shape]);
3073
+ }
3074
+ /**
3075
+ * **Whether a value of this type can be anything but text, so a parameter carried in text needs
3076
+ * reading** - see `wireRead`, which reads it against the schema. A type all of whose values are
3077
+ * strings is left without a reader, and its validator is the schema alone.
918
3078
  *
919
3079
  * A path, query or header value is always a string on the wire, so a numeric or boolean parameter has
920
- * to be decoded before the document's schema runs. Deciding WHICH decoder applies used to be done by
3080
+ * to be decoded before the document's schema runs. Deciding WHETHER it is used to be done by
921
3081
  * inspecting the emitted Zod - `declared.startsWith("z.number()")` - which is a string about a string
922
3082
  * and fails silently the moment the expression is spelled any other way.
923
3083
  *
@@ -928,9 +3088,12 @@ function modelToZod(program, model) {
928
3088
  * is a number - the disagreement is with the wire.
929
3089
  *
930
3090
  * Reading the TYPE answers the question the emitted text was standing in for. `SCALARS` remains the
931
- * single list of what becomes a number.
3091
+ * single list of what becomes a number. **And WHICH reading is the value's is the schema's to say**,
3092
+ * not a kind read from the type: a kind could not say `int32 | boolean`, a numeric enum, or
3093
+ * `int32 | "all"`, and each refused a value the document accepts.
932
3094
  *
933
- * **It still inspects the emitted expression to decide WHICH kind, and that did drift once more.**
3095
+ * **It still inspects the emitted expression to decide whether a scalar is numeric, and that did
3096
+ * drift once more.**
934
3097
  * The test was `startsWith("z.number()")`, and giving each integer width its declared bounds spelled
935
3098
  * nine of them `z.int(`, `z.int32(` and `z.uint32(` instead. Every one silently lost its wire
936
3099
  * decoder: 22 arms went red at once, which is the only reason it was not shipped. The prefixes live
@@ -947,41 +3110,49 @@ function modelToZod(program, model) {
947
3110
  * have to agree and nothing about the two being in the same file makes them.
948
3111
  */
949
3112
  const NUMERIC_EXPRESSIONS = ["z.number()", "z.int(", "z.int32(", "z.uint32("];
950
- export function wireKindOf(program, type) {
951
- if (type.kind === "Scalar") {
952
- const encoded = encodedTypeOf(program, type);
953
- if (encoded !== undefined && encoded !== type)
954
- return wireKindOf(program, encoded);
955
- for (let current = type; current !== undefined; current = current.baseScalar) {
956
- const mapped = SCALARS[current.name];
957
- if (mapped === undefined)
958
- continue;
959
- if (NUMERIC_EXPRESSIONS.some((prefix) => mapped.startsWith(prefix)))
960
- return "number";
961
- if (mapped === "z.boolean()")
962
- return "boolean";
963
- return undefined;
964
- }
965
- return undefined;
966
- }
967
- // A literal is its own type, and a union of them is the enum form a parameter commonly takes.
968
- if (type.kind === "Number")
969
- return "number";
970
- if (type.kind === "Boolean")
971
- return "boolean";
972
- if (type.kind === "Union") {
973
- const kinds = new Set();
974
- for (const variant of type.variants.values()) {
975
- // `T | null` is still a T on the wire; the null arm needs no decoding.
976
- if (variant.type.kind === "Intrinsic" && variant.type.name === "null")
977
- continue;
978
- kinds.add(wireKindOf(program, variant.type));
3113
+ export function readsFromText(program, type) {
3114
+ switch (type.kind) {
3115
+ case "Scalar": {
3116
+ // Its own encoding or an inherited one - see `scalarEncodingOf`.
3117
+ const encoded = scalarEncodingOf(program, type)?.encoded;
3118
+ // A scalar that is not standard states no type, and the schema walk refuses it.
3119
+ if (encoded !== undefined && !program.checker.isStdType(encoded))
3120
+ return false;
3121
+ if (encoded !== undefined)
3122
+ return readsFromText(program, encoded);
3123
+ for (let current = type; current !== undefined; current = current.baseScalar) {
3124
+ const mapped = standardScalarSchemaOf(program, current);
3125
+ if (mapped === undefined)
3126
+ continue;
3127
+ return (NUMERIC_EXPRESSIONS.some((prefix) => mapped.startsWith(prefix)) ||
3128
+ mapped === "z.boolean()");
3129
+ }
3130
+ // A scalar with no standard base is any value, which text can spell as a number.
3131
+ return isTypelessMember(program, type);
979
3132
  }
980
- if (kinds.size !== 1)
981
- return undefined;
982
- return [...kinds][0];
3133
+ /**
3134
+ * **A value of any type is read from text as one too**: `@maxLength(2) a?: string | unknown`
3135
+ * sent `?a=12345` is the number the document reads, which no `maxLength` constrains. Left as
3136
+ * text, it was refused as too long.
3137
+ */
3138
+ case "Intrinsic":
3139
+ return type.name === "unknown";
3140
+ // A literal is its own type, and a union of them is the enum form a parameter commonly takes.
3141
+ case "Number":
3142
+ case "Boolean":
3143
+ return true;
3144
+ case "EnumMember":
3145
+ return typeof type.value === "number";
3146
+ case "Enum":
3147
+ return [...type.members.values()].some((member) => typeof member.value === "number");
3148
+ case "UnionVariant":
3149
+ return readsFromText(program, type.type);
3150
+ // Any member that is not text: `int32 | boolean`, `int32 | "all"` - see `wireRead`.
3151
+ case "Union":
3152
+ return [...type.variants.values()].some((variant) => readsFromText(program, variant.type));
3153
+ default:
3154
+ return false;
983
3155
  }
984
- return undefined;
985
3156
  }
986
3157
  /**
987
3158
  * A name as an object key - quoted when it is not a legal JavaScript identifier.
@@ -1065,11 +3236,7 @@ export function jsDocComment(text, indent) {
1065
3236
  * validator that renamed on the way through would be a transform, which JSON Schema cannot express
1066
3237
  * and no document could state - the same reason `@trimmed` was removed.
1067
3238
  */
1068
- function propertyKey(program, property) {
1069
- return objectKey(rawPropertyKey(program, property));
1070
- }
1071
- /** The same wire name, unquoted - for a vocabulary tuple, where the value is data rather than code. */
1072
- function rawPropertyKey(program, property) {
3239
+ export function rawPropertyKey(program, property) {
1073
3240
  return resolveEncodedName(program, property, JSON_MEDIA_TYPE);
1074
3241
  }
1075
3242
  /**
@@ -1149,37 +3316,963 @@ function isStdDateTime(program, scalar) {
1149
3316
  }
1150
3317
  /** Whether a property's value, or a scalar, is a date-time: one of the two, or built on one. */
1151
3318
  function isDateTime(program, target) {
1152
- const type = target.kind === "ModelProperty" ? (nullableInnerOf(target.type) ?? target.type) : target;
3319
+ const type = target.kind === "ModelProperty" ? encodingTargetOf(target.type) : target;
1153
3320
  for (let current = type.kind === "Scalar" ? type : undefined; current !== undefined; current = current.baseScalar) {
1154
3321
  if (isStdDateTime(program, current))
1155
3322
  return true;
1156
3323
  }
1157
- return false;
1158
- }
1159
- export function encodedTypeOf(program, target) {
1160
- const encoded = getEncode(program, target);
1161
- if (encoded === undefined) {
1162
- return target.kind === "ModelProperty" && isDateTimeHeader(program, target)
1163
- ? program.checker.getStdType("string")
1164
- : undefined;
3324
+ return false;
3325
+ }
3326
+ /**
3327
+ * The type a property's `@encode` lands on in the document: the one member beside `null` where
3328
+ * openapi3 applies it there, and the declared type otherwise - see `encodedBesideNullOf`. A named
3329
+ * `union MaybeTime { utcDateTime, null }` is a `$ref`, so `rfc3339` on it publishes `type: string`
3330
+ * over the whole union and refuses `null`; read through the `null`, it was "no change", and `null`
3331
+ * was accepted.
3332
+ */
3333
+ function encodingTargetOf(type) {
3334
+ const beside = encodedBesideNullOf(type);
3335
+ return beside?.length === 1 && beside[0] !== undefined ? beside[0] : type;
3336
+ }
3337
+ /** Whether a property's value, or a scalar, is a `duration`, or built on one. */
3338
+ function isDuration(program, target) {
3339
+ const type = target.kind === "ModelProperty" ? encodingTargetOf(target.type) : target;
3340
+ for (let current = type.kind === "Scalar" ? type : undefined; current !== undefined; current = current.baseScalar) {
3341
+ if (current === program.checker.getStdType("duration"))
3342
+ return true;
3343
+ }
3344
+ return false;
3345
+ }
3346
+ /**
3347
+ * The JSON type a value is published as before its own `@encode` replaces it: a property's declared
3348
+ * scalar, or the base a scalar is built from. `undefined` where it is not a scalar.
3349
+ */
3350
+ function typeBeforeEncoding(program, target) {
3351
+ const type = target.kind === "ModelProperty" ? encodingTargetOf(target.type) : target.baseScalar;
3352
+ return type?.kind === "Scalar" ? jsonTypeOfScalar(program, type) : undefined;
3353
+ }
3354
+ export function encodedTypeOf(program, target) {
3355
+ const encoded = getEncode(program, target);
3356
+ if (encoded === undefined) {
3357
+ return target.kind === "ModelProperty" && isDateTimeHeader(program, target)
3358
+ ? program.checker.getStdType("string")
3359
+ : undefined;
3360
+ }
3361
+ /**
3362
+ * **`rfc3339` on a date-time is the form it already has, so it changes nothing.** openapi3's
3363
+ * `mergeFormatAndEncoding` keeps `format: date-time` for it, and the type it reports, `string`,
3364
+ * emitted `z.string()`: the explicit spelling of the default dropped the date-time check the
3365
+ * document still states. Only the encodings that change the wire form - `rfc7231`,
3366
+ * `unixTimestamp` - change the type.
3367
+ *
3368
+ * **Only where the value already IS that string.** `@encode(rfc3339) at: Ts`, with `Ts` a
3369
+ * date-time `@encode`d as an integer, publishes `{ $ref: Ts, type: "string" }`, which no value
3370
+ * satisfies; read as "no change" it validated `Ts`'s integer and contradicted the document. The
3371
+ * same holds for `ISO8601` on a duration, the form `duration` already has.
3372
+ */
3373
+ const before = typeBeforeEncoding(program, target);
3374
+ const unchanged = (encoded.encoding === "rfc3339" && isDateTime(program, target)) ||
3375
+ (encoded.encoding === "ISO8601" && isDuration(program, target));
3376
+ if (unchanged && before === "string")
3377
+ return undefined;
3378
+ if (target.kind === "ModelProperty" &&
3379
+ isQueryParam(program, target) &&
3380
+ PARAMETER_STYLE_ENCODINGS.has(encoded.encoding ?? "")) {
3381
+ return undefined;
3382
+ }
3383
+ return encoded.type;
3384
+ }
3385
+ export function encodedValueOf(program, property) {
3386
+ const scalar = encodedTypeOf(program, property);
3387
+ if (scalar === undefined)
3388
+ return { kind: "declared" };
3389
+ const declared = property.type;
3390
+ if (!program.checker.isStdType(scalar)) {
3391
+ return { kind: "unsatisfiable", scalar, declared, why: "typeless" };
3392
+ }
3393
+ const besideNull = encodedBesideNullOf(declared);
3394
+ const nullable = besideNull !== undefined;
3395
+ const inner = besideNull ?? [declared];
3396
+ const whole = inner.length === 1 ? inner[0] : undefined;
3397
+ if (whole !== undefined && !keepsDeclaredType(program, whole)) {
3398
+ return { kind: "replaced", scalar, nullable };
3399
+ }
3400
+ // A standard scalar always has a JSON type.
3401
+ const target = jsonTypeOfScalar(program, scalar) ?? "string";
3402
+ const members = whole === undefined ? inner : membersOf(whole);
3403
+ const kept = members.filter((member) => admits(program, member, target));
3404
+ if (kept.length === 0) {
3405
+ return { kind: "unsatisfiable", scalar, declared, why: nullable ? "null" : "nothing" };
3406
+ }
3407
+ return {
3408
+ kind: "both",
3409
+ scalar,
3410
+ form: getEncode(program, property)?.encoding,
3411
+ nullable,
3412
+ /**
3413
+ * The declared type whole where nothing was ruled out and it holds no scalar of its own -
3414
+ * so a named enum or union of literals is still referenced - and its members otherwise, so a
3415
+ * member scalar can be read the way `keptMemberToZod` reads it.
3416
+ */
3417
+ kept: whole !== undefined &&
3418
+ kept.length === members.length &&
3419
+ (whole.kind === "Scalar" || !members.some((member) => member.kind === "Scalar"))
3420
+ ? whole
3421
+ : kept,
3422
+ within: kept.every((member) => isWithin(program, member, target)),
3423
+ };
3424
+ }
3425
+ /**
3426
+ * **The members beside `null` that openapi3 3.1 applies an encoding to, where it applies it to them
3427
+ * rather than to the whole union.**
3428
+ *
3429
+ * Its `applyEncoding` takes that path only for an `anyOf` of exactly two members, one of them
3430
+ * `{ type: "null" }`. The members are what it PUBLISHES, not the variants written: literals of one
3431
+ * kind are merged into a single `enum`, so `"a" | "b" | null` is two members and `null` survives the
3432
+ * encoding, while `"a" | 1 | null` is three and the encoded `type` refuses `null`. A named union is
3433
+ * published as a `$ref`, never as that `anyOf`. Read off the variants, `"a" | "b" | null` refused the
3434
+ * `null` the document allows.
3435
+ */
3436
+ function encodedBesideNullOf(type) {
3437
+ if (type.kind !== "Union" || isPublishedAsReference(type))
3438
+ return undefined;
3439
+ const variants = [...type.variants.values()].map((variant) => variant.type);
3440
+ const isNull = (each) => each.kind === "Intrinsic" && each.name === "null";
3441
+ const nulls = variants.filter(isNull).length;
3442
+ if (nulls !== 1)
3443
+ return undefined;
3444
+ const rest = variants.filter((each) => !isNull(each));
3445
+ const isLiteral = (each) => each.kind === "String" || each.kind === "Number" || each.kind === "Boolean";
3446
+ // Each `null` is a member of its own: `"a" | null | null` publishes three, and the encoding lands
3447
+ // on the whole `anyOf`.
3448
+ const published = new Set(rest.filter(isLiteral).map((each) => each.kind)).size +
3449
+ rest.filter((each) => !isLiteral(each)).length;
3450
+ return published === 1 ? rest : undefined;
3451
+ }
3452
+ /**
3453
+ * **Whether openapi3 publishes a union as a `$ref` to a component, rather than inline.**
3454
+ *
3455
+ * A named union is a component; an anonymous one is written out where it is used. A union template
3456
+ * instance is a component only where every argument has a name of its own to build the component's
3457
+ * name from - `Opt<int32>` is `OptInt32` - and is written out inline otherwise, as `Opt<"a">` is:
3458
+ * the emitter that names declarations calls such an instance "unspeakable". Read as a `$ref`, the
3459
+ * `null` that `Opt<"a">` publishes beside `"a"` was refused.
3460
+ */
3461
+ function isPublishedAsReference(union) {
3462
+ if (union.name === undefined || union.name === "")
3463
+ return false;
3464
+ return union.templateMapper === undefined || union.templateMapper.args.every(isSpeakable);
3465
+ }
3466
+ /** Whether a template argument has a declaration name openapi3 can build a component name from. */
3467
+ function isSpeakable(argument) {
3468
+ const type = (argument.entityKind === "Indeterminate"
3469
+ ? argument.type
3470
+ : argument);
3471
+ if (type === undefined || !("kind" in type))
3472
+ return false;
3473
+ switch (type.kind) {
3474
+ case "Model":
3475
+ case "Scalar":
3476
+ case "Interface":
3477
+ case "Operation":
3478
+ case "Enum":
3479
+ case "Union":
3480
+ case "Intrinsic":
3481
+ if (!("name" in type) || type.name === undefined || type.name === "")
3482
+ return false;
3483
+ return (!("templateMapper" in type) ||
3484
+ type.templateMapper === undefined ||
3485
+ type.templateMapper.args.every(isSpeakable));
3486
+ default:
3487
+ return false;
3488
+ }
3489
+ }
3490
+ /**
3491
+ * Whether openapi3 keeps the declared type's schema beside an encoding in a way that still
3492
+ * constrains the value: a `$ref` to anything it declares - a model, an enum, a named union, a scalar
3493
+ * outside the standard library - or an inline union or literal, whose `anyOf` or `enum` stays.
3494
+ *
3495
+ * **Declared means what `@typespec/openapi` says is declared**, through its own `shouldInline`. An
3496
+ * inline list, record or object - `{ ...Filter }` among them, which has no name of its own - keeps
3497
+ * only keywords a scalar is not checked against. This read the emitter's own naming, which names a
3498
+ * spread after the model spread into it, and refused `{ ...Filter }` as unsatisfiable while the
3499
+ * document publishes it inline and accepts any string.
3500
+ */
3501
+ function keepsDeclaredType(program, declared) {
3502
+ switch (declared.kind) {
3503
+ case "Enum":
3504
+ case "EnumMember":
3505
+ case "Union":
3506
+ case "String":
3507
+ case "Number":
3508
+ case "Boolean":
3509
+ return true;
3510
+ case "Scalar":
3511
+ case "Model":
3512
+ return !shouldInline(program, declared);
3513
+ default:
3514
+ return false;
3515
+ }
3516
+ }
3517
+ /** A declared type as the members its schema lists: a union's variants, an enum's members. */
3518
+ /**
3519
+ * **A union's members, a nested union spelled out only where it combines as the union does** -
3520
+ * `anyOf` inside `anyOf`, `oneOf` inside `oneOf`. A nested union of the other kind is one member,
3521
+ * whole: `@oneOf` over `int32 | float` and a `string` is exactly one of two, where spelled out it was
3522
+ * exactly one of three and an integer, both an `int32` and a `float`, was none; and a `@oneOf` inside
3523
+ * a plain union, spelled out, admitted the value two of its members accept.
3524
+ */
3525
+ function alikeMembersOf(program, type, exclusive = isExclusive(program, type)) {
3526
+ if (type.kind === "UnionVariant")
3527
+ return alikeMembersOf(program, type.type, exclusive);
3528
+ if (type.kind === "Enum")
3529
+ return [...type.members.values()];
3530
+ if (type.kind !== "Union")
3531
+ return [type];
3532
+ /**
3533
+ * **A nested union is one member unless both sides are `anyOf`**: `anyOf(anyOf(a, b), c)` is
3534
+ * `anyOf(a, b, c)`, but `oneOf(oneOf(a, b), c)` is not `oneOf(a, b, c)` - `1` matches both of
3535
+ * `int32 | float` and so fails the inner union, and is valid by `c` alone. Spelled out, `1` was
3536
+ * refused where the document accepts it.
3537
+ */
3538
+ const members = [...type.variants.values()].flatMap((variant) => {
3539
+ const member = variant.type;
3540
+ return member.kind === "Union" && (exclusive || isPublishedOneOf(program, member))
3541
+ ? [member]
3542
+ : alikeMembersOf(program, member, exclusive);
3543
+ });
3544
+ return exclusive ? withoutRepeatedLiterals(members) : members;
3545
+ }
3546
+ /**
3547
+ * **A literal repeated in a `@oneOf` is one member**, as openapi3 publishes every literal of one
3548
+ * JSON kind as one `enum`: `@oneOf union D { a: 1, b: 1, c: string }` is
3549
+ * `oneOf: [{ enum: [1, 1] }, { type: string }]`, which `1` matches once. Each its own member, `1`
3550
+ * matched two, and was refused.
3551
+ */
3552
+ function withoutRepeatedLiterals(members) {
3553
+ const seen = new Set();
3554
+ return members.filter((member) => {
3555
+ const value = isEnvelope(member) || member.kind === "EnumMember" ? undefined : literalValueOf(member);
3556
+ if (value === undefined)
3557
+ return true;
3558
+ const key = `${typeof value}\u0000${String(value)}`;
3559
+ if (seen.has(key))
3560
+ return false;
3561
+ seen.add(key);
3562
+ return true;
3563
+ });
3564
+ }
3565
+ /** Whether `null` is a value of a type: a member, once under `@oneOf`, or a member with no type. */
3566
+ function admitsNullValue(program, type, exclusive = isExclusive(program, type)) {
3567
+ if (type.kind === "UnionVariant")
3568
+ return admitsNullValue(program, type.type);
3569
+ if (isNull(type) || isTypelessMember(program, type))
3570
+ return true;
3571
+ if (type.kind !== "Union")
3572
+ return false;
3573
+ const matches = alikeMembersOf(program, type, exclusive).filter((member) => admitsNullValue(program, member)).length;
3574
+ return exclusive ? matches === 1 : matches > 0;
3575
+ }
3576
+ function membersOf(type) {
3577
+ // `DogKind.Golden` names a variant of a union, and stands for its type.
3578
+ if (type.kind === "UnionVariant")
3579
+ return membersOf(type.type);
3580
+ if (type.kind === "Union")
3581
+ return [...type.variants.values()].flatMap((v) => membersOf(v.type));
3582
+ if (type.kind === "Enum")
3583
+ return [...type.members.values()];
3584
+ return [type];
3585
+ }
3586
+ /** Whether a member of the declared type has a value of the encoded scalar's JSON type. */
3587
+ function admits(program, member, target) {
3588
+ const types = jsonTypesOf(program, member);
3589
+ if (types === undefined)
3590
+ return true;
3591
+ return [...types].some((type) => type === target ||
3592
+ (target === "integer" && type === "number") ||
3593
+ (target === "number" && (type === "integer" || type === "fraction")));
3594
+ }
3595
+ /** Whether every value of a member is of the encoded scalar's JSON type, so it alone says both. */
3596
+ function isWithin(program, member, target) {
3597
+ const types = jsonTypesOf(program, member);
3598
+ if (types === undefined)
3599
+ return false;
3600
+ return [...types].every((type) => type === target || (target === "number" && (type === "integer" || type === "fraction")));
3601
+ }
3602
+ const INTEGER_SCALARS = [
3603
+ "integer",
3604
+ "int8",
3605
+ "int16",
3606
+ "int32",
3607
+ "int64",
3608
+ "uint8",
3609
+ "uint16",
3610
+ "uint32",
3611
+ "uint64",
3612
+ "safeint",
3613
+ ];
3614
+ const NUMBER_SCALARS = ["numeric", "float", "float32", "float64", "decimal", "decimal128"];
3615
+ /**
3616
+ * The JSON type a scalar is published as, its own scalar-level `@encode` included - or `undefined`
3617
+ * for a scalar with no standard base, which openapi3 publishes as `{}`, any value. Read as a string,
3618
+ * `@encode("x", int32)` on one was refused as unsatisfiable while the document accepts `5`.
3619
+ */
3620
+ export function jsonTypeOfScalar(program, scalar) {
3621
+ const { checker } = program;
3622
+ for (let current = scalar; current !== undefined; current = current.baseScalar) {
3623
+ const encoded = encodedTypeOf(program, current);
3624
+ if (encoded !== undefined && encoded !== current && checker.isStdType(encoded)) {
3625
+ return jsonTypeOfScalar(program, encoded);
3626
+ }
3627
+ const link = current;
3628
+ if (INTEGER_SCALARS.some((name) => link === checker.getStdType(name)))
3629
+ return "integer";
3630
+ if (NUMBER_SCALARS.some((name) => link === checker.getStdType(name)))
3631
+ return "number";
3632
+ if (link === checker.getStdType("boolean"))
3633
+ return "boolean";
3634
+ if (checker.isStdType(link))
3635
+ return "string";
3636
+ }
3637
+ return undefined;
3638
+ }
3639
+ /** The JSON types a declared type admits, or `undefined` where it admits any. */
3640
+ function jsonTypesOf(program, type) {
3641
+ const numeric = (value) => (Number.isInteger(value) ? "integer" : "fraction");
3642
+ switch (type.kind) {
3643
+ case "Scalar": {
3644
+ const json = jsonTypeOfScalar(program, type);
3645
+ return json === undefined ? undefined : new Set([json]);
3646
+ }
3647
+ case "String":
3648
+ return new Set(["string"]);
3649
+ case "Number":
3650
+ return new Set([numeric(type.value)]);
3651
+ case "Boolean":
3652
+ return new Set(["boolean"]);
3653
+ case "EnumMember":
3654
+ return new Set([typeof type.value === "number" ? numeric(type.value) : "string"]);
3655
+ case "Enum":
3656
+ case "Union": {
3657
+ const types = new Set();
3658
+ for (const member of membersOf(type)) {
3659
+ const memberTypes = jsonTypesOf(program, member);
3660
+ if (memberTypes === undefined)
3661
+ return undefined;
3662
+ for (const each of memberTypes)
3663
+ types.add(each);
3664
+ }
3665
+ return types;
3666
+ }
3667
+ case "Tuple":
3668
+ return new Set(["array"]);
3669
+ case "Model":
3670
+ return new Set([type.indexer?.key.name === "integer" ? "array" : "object"]);
3671
+ case "Intrinsic":
3672
+ return type.name === "null" ? new Set(["null"]) : undefined;
3673
+ default:
3674
+ return undefined;
3675
+ }
3676
+ }
3677
+ /** `unsatisfiable-encoding`, once per declaration however many positions render it. */
3678
+ function reportUnsatisfiableEncoding(program, target, format) {
3679
+ if (!firstReportOf(program, "unsatisfiable-encoding", target, `${format.declared} ${format.encoded}`)) {
3680
+ return;
3681
+ }
3682
+ const why = {
3683
+ typeless: `'${format.encoded}' is not a standard scalar, so '@typespec/openapi3' publishes no type for it and the document accepts any value`,
3684
+ nothing: "'@typespec/openapi3' keeps the declared type beside the encoded one, and no value is both, so the document accepts nothing",
3685
+ null: "'@typespec/openapi3' keeps the declared type beside the encoded one, and no value but null is both, so the document accepts only null",
3686
+ }[format.why];
3687
+ reportDiagnostic(program, {
3688
+ code: "unsatisfiable-encoding",
3689
+ target,
3690
+ format: { name: format.name, declared: format.declared, encoded: format.encoded, why },
3691
+ });
3692
+ }
3693
+ /** A type's name for a message: its own where it has one, else its spelling. */
3694
+ function nameOfType(type) {
3695
+ return "name" in type && typeof type.name === "string" && type.name !== ""
3696
+ ? type.name
3697
+ : getTypeName(type);
3698
+ }
3699
+ /**
3700
+ * **Whether an encoded value is written as the encoded scalar intersected with what it keeps.**
3701
+ *
3702
+ * Where the kept type alone says both it is written alone - but a constraint on the property cannot
3703
+ * go on a kept union or literal, which carries no `.min` or `.regex`, so then the constraints go on
3704
+ * the encoded scalar and the kept type is intersected beside it. Applied to the union,
3705
+ * `@encode("x", string) @minLength(2) v: string | "a"` emitted `z.union(...).min(2)`, and the module
3706
+ * threw on import.
3707
+ */
3708
+ export function encodedIntersects(program, property, encoding) {
3709
+ if (!encoding.within)
3710
+ return true;
3711
+ const kept = encoding.kept;
3712
+ const scalar = !Array.isArray(kept) && kept.kind === "Scalar";
3713
+ return hasConstraints(program, property) && !scalar;
3714
+ }
3715
+ /**
3716
+ * **A union property's constraints, applied to each member as JSON Schema applies them.**
3717
+ *
3718
+ * `@minLength(2) v: string | "a" | int32` is published `{ anyOf: [...], minLength: 2 }`, and
3719
+ * `minLength` constrains a value only where it is a string: `"ab"` and `5` are valid, `"a"` is not.
3720
+ * Each member takes the keywords of its own JSON type, and a literal member breaking one is no value
3721
+ * at all. Applied to the union, this emitted `z.union(...).min(2)`, and the module threw on import.
3722
+ * The constraints are the property's own and those of every declaration above it the position
3723
+ * leaves - see `PropertyPosition` - and, where openapi3 adds the discriminator as a `string`, only
3724
+ * the members that can be a string are left. Returns the members left, or `undefined` where the
3725
+ * property is not a union read member by member.
3726
+ */
3727
+ export function constrainedMembersOf(program, property, position = OWN_POSITION) {
3728
+ const declared = nullableInnerOf(property.type) ?? property.type;
3729
+ const sources = [property, ...position.constrainedBy];
3730
+ const constrained = sources.some((source) => hasConstraints(program, source));
3731
+ const typeless = isTypelessMember(program, declared);
3732
+ /**
3733
+ * A value with no type of its own, constrained or narrowed to a string, is read as its kinds. An
3734
+ * enum a keyword constrains is read as its members, as a union of literals is: `e: Color` under a
3735
+ * declaration's `@maxLength(3)` keeps `red`, where the keyword applied to the enum's schema was
3736
+ * `colorSchema.max(3)`, and the module threw on import.
3737
+ */
3738
+ const members = declared.kind === "Union" || (declared.kind === "Enum" && constrained);
3739
+ if (!members && !(typeless && (position.stringOnly || constrained))) {
3740
+ return undefined;
3741
+ }
3742
+ // A `null` a declaration above refuses is no member.
3743
+ const dropsNull = position.nonNull && hasNull(declared);
3744
+ if (declared.kind === "Union" && !constrained && !position.stringOnly && !dropsNull) {
3745
+ return undefined;
3746
+ }
3747
+ const string = program.checker.getStdType("string");
3748
+ const exclusive = isExclusive(program, declared) || isExclusive(program, property);
3749
+ return (alikeMembersOf(program, declared, exclusive)
3750
+ .flatMap((member) => {
3751
+ if (!position.nonNull)
3752
+ return [member];
3753
+ const kept = withoutNull(program, member);
3754
+ return kept === undefined ? [] : [kept];
3755
+ })
3756
+ .map((member) => (position.stringOnly && isTypelessMember(program, member) ? string : member))
3757
+ .filter((member) => !position.stringOnly || admits(program, member, "string"))
3758
+ /**
3759
+ * A literal a declaration above refuses is no member: by its keywords, or by the check its
3760
+ * declared scalar emits. Each member is then exactly its values the position admits, so a
3761
+ * `@oneOf` counts a value as before: `@oneOf union O1 { Color, string }` under `k: utcDateTime`
3762
+ * kept `"red"`, matched by no other narrowed member, where the document finds it in two.
3763
+ */
3764
+ .filter((member) => {
3765
+ const value = literalValueOf(member);
3766
+ return (value === undefined ||
3767
+ sources.every((source) => brokenConstraintOf(program, source, value) === undefined &&
3768
+ (source.kind !== "Scalar" || scalarAccepts(program, source, value) !== false)));
3769
+ }));
3770
+ }
3771
+ /**
3772
+ * **A member without its `null`, where a declaration above refuses `null`**, or `undefined` where
3773
+ * nothing is left. A `@oneOf` keeps a nested union whole - see `alikeMembersOf` - so the `null` of
3774
+ * `@oneOf union O8 { v0: "x" | null, v1: "y" }` sat inside a member, and under `c: string` the
3775
+ * value accepted the `null` the document refuses. Taken out of the member it is sound: a value other
3776
+ * than `null` matches each member exactly as before, so a `@oneOf` counts it the same.
3777
+ */
3778
+ function withoutNull(program, member) {
3779
+ const type = member.kind === "UnionVariant" ? member.type : member;
3780
+ if (isNull(type))
3781
+ return undefined;
3782
+ if (type.kind !== "Union" || !hasNull(type))
3783
+ return member;
3784
+ const kept = [...type.variants.values()].flatMap((variant) => {
3785
+ const each = withoutNull(program, variant.type);
3786
+ return each === undefined ? [] : [each];
3787
+ });
3788
+ if (kept.length <= 1)
3789
+ return kept[0];
3790
+ const union = $(program).union.create(kept);
3791
+ if (isExclusive(program, type))
3792
+ exclusiveWithoutNull.add(union);
3793
+ return union;
3794
+ }
3795
+ /** The unions {@link withoutNull} builds from a `@oneOf`, which stay exclusive without the decorator. */
3796
+ const exclusiveWithoutNull = new WeakSet();
3797
+ /** A member with no JSON type of its own: `unknown`, or a scalar with no standard base. */
3798
+ export function isTypelessMember(program, member) {
3799
+ if (member.kind === "Intrinsic")
3800
+ return member.name === "unknown";
3801
+ return member.kind === "Scalar" && jsonTypeOfScalar(program, member) === undefined;
3802
+ }
3803
+ /**
3804
+ * **Any JSON value, kind by kind** - what a member with no type is, where a keyword constrains the
3805
+ * kinds it applies to: `@minLength(2) v: string | unknown` refuses `"a"`, which `unknown` alone
3806
+ * accepted. TypeScript states the same union.
3807
+ */
3808
+ const ANY_JSON = [
3809
+ ["z.string()", "string", "string"],
3810
+ ["z.number()", "number", "number"],
3811
+ ["z.boolean()", "boolean", "boolean"],
3812
+ ["z.null()", "null", "null"],
3813
+ ["z.array(z.unknown())", "array", "unknown[]"],
3814
+ ["z.record(z.string(), z.unknown())", "object", "globalThis.Record<string, unknown>"],
3815
+ ];
3816
+ export const ANY_JSON_TS = ANY_JSON.map(([, , ts]) => ts).join(" | ");
3817
+ /**
3818
+ * A value with the constraints of the declarations above it applied - see `PropertyPosition` - for
3819
+ * the value's own JSON type, as JSON Schema applies them through `allOf`.
3820
+ */
3821
+ function withAncestors(program, value, property, position) {
3822
+ const json = ownJsonTypeOf(program, property);
3823
+ return position.constrainedBy.reduce((each, ancestor) => applyConstraints(program, each, ancestor, json === undefined ? { quiet: true } : { type: json, quiet: true }), value);
3824
+ }
3825
+ /** The JSON type of a property's own value, where one keyword family applies to it. */
3826
+ function ownJsonTypeOf(program, property) {
3827
+ const published = publishedJsonTypeOf(program, property);
3828
+ if (published !== undefined)
3829
+ return published;
3830
+ const declared = nullableInnerOf(property.type) ?? property.type;
3831
+ if (declared.kind === "Model")
3832
+ return declared.indexer?.key.name === "integer" ? "array" : "object";
3833
+ if (declared.kind === "Tuple")
3834
+ return "array";
3835
+ return isNull(declared) ? "null" : undefined;
3836
+ }
3837
+ /** `unrepresentable-override`, once per declaration and ancestor. */
3838
+ function reportUnrepresentableOverride(program, property, position) {
3839
+ const refused = position.refused;
3840
+ if (refused === undefined)
3841
+ return;
3842
+ const owner = refused.ancestor.model?.name ?? "its base";
3843
+ if (!firstReportOf(program, "unrepresentable-override", property, owner))
3844
+ return;
3845
+ reportDiagnostic(program, {
3846
+ code: "unrepresentable-override",
3847
+ messageId: refused.recursive ? "recursive" : "default",
3848
+ target: property,
3849
+ format: {
3850
+ name: property.name,
3851
+ model: property.model?.name ?? "",
3852
+ base: owner,
3853
+ declared: getTypeName(refused.ancestor.type, { namespaceFilter: () => false }),
3854
+ why: refused.why,
3855
+ },
3856
+ });
3857
+ }
3858
+ /** A literal member's value, or `undefined` where the member is not a literal. */
3859
+ function literalValueOf(type) {
3860
+ switch (type.kind) {
3861
+ case "String":
3862
+ case "Number":
3863
+ case "Boolean":
3864
+ return type.value;
3865
+ case "EnumMember":
3866
+ return type.value ?? type.name;
3867
+ default:
3868
+ return undefined;
3869
+ }
3870
+ }
3871
+ /** One member of a constrained union, with each source's keywords for its JSON type. */
3872
+ function constrainedMemberToZod(program, member, sources) {
3873
+ const constrain = (schema, type) => sources.reduce((value, source) => applyConstraints(program, value, source, { type, quiet: true }), schema);
3874
+ if (isTypelessMember(program, member)) {
3875
+ return `z.union([${ANY_JSON.map(([schema, json]) => constrain(schema, json)).join(", ")}])`;
3876
+ }
3877
+ const schema = typeToZod(program, member);
3878
+ if (literalValueOf(member) !== undefined)
3879
+ return schema;
3880
+ const type = member.kind === "Scalar"
3881
+ ? jsonTypeOfScalar(program, member)
3882
+ : member.kind === "Tuple" ||
3883
+ (member.kind === "Model" && member.indexer?.key.name === "integer")
3884
+ ? "array"
3885
+ : member.kind === "Intrinsic" && member.name === "null"
3886
+ ? "null"
3887
+ : member.kind === "Model"
3888
+ ? "object"
3889
+ : undefined;
3890
+ return type === undefined ? schema : constrain(schema, type);
3891
+ }
3892
+ /**
3893
+ * What an encoded property keeps of its declared type, as Zod: the whole type where none of it was
3894
+ * ruled out, else the members of the encoded scalar's JSON type.
3895
+ *
3896
+ * **A declared scalar of the encoded scalar's JSON type keeps its keywords and not its format.**
3897
+ * openapi3 publishes `@encode(rfc7231) at: Instant` as `{ $ref: Instant, type: "string" }`, and
3898
+ * `Instant`'s own check - an ISO date-time - is the wire form the encoding replaced: an HTTP-date is
3899
+ * the value now. What the reference still asserts is `Instant`'s constraints. Validated as `Instant`,
3900
+ * every HTTP-date was refused.
3901
+ */
3902
+ function keptToZod(program, encoding, schemaOf) {
3903
+ if (Array.isArray(encoding.kept)) {
3904
+ return membersToZod(program, encoding.kept, (member) => keptMemberToZod(program, encoding, member) ?? typeToZod(program, member));
3905
+ }
3906
+ const kept = encoding.kept;
3907
+ return keptMemberToZod(program, encoding, kept) ?? schemaOf(kept);
3908
+ }
3909
+ /**
3910
+ * A declared scalar of the encoded scalar's JSON type, as the encoded scalar plus the declared one's
3911
+ * constraints - or `undefined` where the member is read as itself. The encoding replaced the form the
3912
+ * scalar's own check is for, unless it IS that form: `rfc3339` on a date-time, `ISO8601` on a
3913
+ * duration.
3914
+ */
3915
+ function keptMemberToZod(program, encoding, member) {
3916
+ return replacesFormOf(program, encoding, member)
3917
+ ? applyConstraints(program, scalarSchemaOf(program, encoding.scalar), member)
3918
+ : undefined;
3919
+ }
3920
+ /** Whether a kept member is a scalar whose own check the encoding's wire form replaces. */
3921
+ export function replacesFormOf(program, encoding, member) {
3922
+ if (member.kind !== "Scalar")
3923
+ return false;
3924
+ if (jsonTypeOfScalar(program, member) !== jsonTypeOfScalar(program, encoding.scalar))
3925
+ return false;
3926
+ const ownForm = (encoding.form === "rfc3339" && isDateTime(program, member)) ||
3927
+ (encoding.form === "ISO8601" && isDuration(program, member));
3928
+ return !ownForm;
3929
+ }
3930
+ /** Members of a declared type, as the one schema a union of them is. */
3931
+ function membersToZod(program, members, render = (member) => typeToZod(program, member), exclusive = false) {
3932
+ if (members.every((member) => member.kind === "String")) {
3933
+ const literals = members.map((member) => literal(member.value));
3934
+ return `z.enum([${literals.join(", ")}])`;
3935
+ }
3936
+ const [only] = members;
3937
+ if (members.length === 1 && only !== undefined)
3938
+ return render(only);
3939
+ return `${exclusive ? "z.xor" : "z.union"}([${members.map(render).join(", ")}])`;
3940
+ }
3941
+ /**
3942
+ * **Whether a union is published as `oneOf`, where exactly one member may accept a value.**
3943
+ * `@oneOf` is `@typespec/openapi3`'s decorator, on a union or on a property whose inline union it
3944
+ * makes `oneOf`; openapi3 publishes `anyOf` otherwise. Read by its state key, which the library mints
3945
+ * with `Symbol.for`, so this package depends on nothing it does not already. Validated as `z.union`,
3946
+ * a value two members accept was accepted where the document refuses it; `z.xor` is the exclusive
3947
+ * union.
3948
+ */
3949
+ /**
3950
+ * **Whether openapi3 publishes a union as `oneOf`**: a `@oneOf`, or a `@discriminated` union, whose
3951
+ * variants it publishes as `oneOf` beside the discriminator mapping. A nested one is a member of its
3952
+ * own, whole: spread into `DN | O2`, `@discriminated union DN { a: NA, b: NB }` lost its
3953
+ * exclusivity, and under a PATCH `{}` - which both `NAUpdate` and `NBUpdate` accept, so `DNUpdate`
3954
+ * refuses - was accepted.
3955
+ */
3956
+ function isPublishedOneOf(program, union) {
3957
+ return isExclusive(program, union) || getDiscriminatedUnion(program, union)[0] !== undefined;
3958
+ }
3959
+ export function isExclusive(program, target) {
3960
+ return (program.stateMap(Symbol.for("@typespec/openapi3.oneOf")).get(target) === true ||
3961
+ exclusiveWithoutNull.has(target));
3962
+ }
3963
+ /** The inline union a property's `@oneOf` makes exclusive, while the property's value renders. */
3964
+ let exclusiveSite;
3965
+ /** Whether a union is exclusive where it is rendered: its own `@oneOf`, or its property's. */
3966
+ export function isExclusiveHere(program, union) {
3967
+ return isExclusive(program, union) || exclusiveSite === union;
3968
+ }
3969
+ /** The inline union a property's `@oneOf` makes exclusive; a named one is a `$ref` it leaves alone. */
3970
+ export function exclusiveSiteOf(program, property, declared) {
3971
+ return isExclusive(program, property) && declared.kind === "Union" && !declared.name
3972
+ ? declared
3973
+ : undefined;
3974
+ }
3975
+ /**
3976
+ * **Whether a `@oneOf` holds a member whose declaration is refused, or refers to one that is**, so
3977
+ * that it validates nothing. openapi3 still publishes the member, and counts it: `@oneOf union O {
3978
+ * a: S, b: N }` with `S` refused as `split-visibility` accepted `{"id": "i"}`, which matches both
3979
+ * members in the document and so is refused there - `z.xor` counted the refused `S` as matching
3980
+ * nothing. **A member holding a refused declaration further down is held stricter in the same way**:
3981
+ * `@oneOf { v0: M16, v1: M13 }`, with `M13`'s `c?: M14` refused, accepted `{ c: {...}, e: "red" }`,
3982
+ * which matches both in the document, as `M16` alone. Both walks ask it, so the validator and the
3983
+ * contract type are `never` together.
3984
+ */
3985
+ export function holdsRefusedMember(program, union, exclusive) {
3986
+ if (!exclusive)
3987
+ return false;
3988
+ return [...union.variants.values()].some(({ type }) => {
3989
+ if (type.kind !== "Model" && type.kind !== "Union")
3990
+ return false;
3991
+ return held.holdsRefusalAt(type, renderedVisibilityOf(program, type, visibility));
3992
+ });
3993
+ }
3994
+ export function withExclusiveSite(site, run) {
3995
+ const previous = exclusiveSite;
3996
+ exclusiveSite = site;
3997
+ try {
3998
+ return run();
3999
+ }
4000
+ finally {
4001
+ exclusiveSite = previous;
4002
+ }
4003
+ }
4004
+ /**
4005
+ * The members of a union a value's keywords leave, or `undefined` where the type is no union: a
4006
+ * literal member breaking one is no value at all. Shared by both walks, so they list the same members.
4007
+ */
4008
+ export function keptMembersOf(program, type, keywords) {
4009
+ if (type.kind !== "Union" && type.kind !== "Enum")
4010
+ return undefined;
4011
+ return membersOf(type).filter((member) => {
4012
+ const value = literalValueOf(member);
4013
+ return (value === undefined ||
4014
+ keywords.every((source) => brokenConstraintOf(program, source, value) === undefined));
4015
+ });
4016
+ }
4017
+ /** The keywords that constrain anything, of the declarations a merged value carries. */
4018
+ export function constrainingKeywords(program, keywords) {
4019
+ return keywords.filter((source) => hasConstraints(program, source));
4020
+ }
4021
+ /** A type with keywords on it, each applied to a member of its own JSON type. */
4022
+ function typeWithKeywordsToZod(program, type, keywords) {
4023
+ const sources = constrainingKeywords(program, keywords);
4024
+ if (sources.length === 0)
4025
+ return typeToZod(program, type);
4026
+ const members = keptMembersOf(program, type, sources);
4027
+ if (members === undefined)
4028
+ return constrainedMemberToZod(program, type, sources);
4029
+ if (members.length === 0)
4030
+ return UNREPRESENTABLE;
4031
+ return membersToZod(program, members, (member) => constrainedMemberToZod(program, member, sources), isExclusive(program, type));
4032
+ }
4033
+ /** Keywords applied to a value of one JSON type; a model's are its own, read as declared. */
4034
+ function applyKeywords(program, expression, keywords, json) {
4035
+ return keywords.reduce((each, source) => source.kind === "Model"
4036
+ ? applyConstraints(program, each, source)
4037
+ : applyConstraints(program, each, source, { type: json, quiet: true }), expression);
4038
+ }
4039
+ /** A merged value as Zod - see {@link Merged}. */
4040
+ function mergedToZod(program, value) {
4041
+ switch (value.kind) {
4042
+ case "never":
4043
+ case "refused":
4044
+ return UNREPRESENTABLE;
4045
+ case "type": {
4046
+ const typed = () => typeWithKeywordsToZod(program, value.type, value.keywords);
4047
+ return value.at === undefined ? typed() : withVisibility(value.at, typed);
4048
+ }
4049
+ case "whole":
4050
+ return propertyValueToZod(program, value.property).base;
4051
+ case "nullable":
4052
+ return `${mergedToZod(program, value.inner)}.nullable()`;
4053
+ case "union": {
4054
+ const strings = value.members.map((member) => member.kind === "type" && member.type.kind === "String" ? member.type.value : undefined);
4055
+ // `z.enum` is one member of each value: an exclusive union matching a value twice refuses it.
4056
+ if (value.exclusive !== true && strings.every((each) => each !== undefined)) {
4057
+ return `z.enum([${strings.map((each) => literal(each)).join(", ")}])`;
4058
+ }
4059
+ const rendered = value.members.map((member) => mergedToZod(program, member));
4060
+ if (value.exclusive === true)
4061
+ return `z.xor([${rendered.join(", ")}])`;
4062
+ const kept = rendered.flatMap((text, index) => rendered.indexOf(text) === index ? [index] : []);
4063
+ noteWrittenMembers(program, value, kept);
4064
+ const members = kept.map((index) => rendered[index]);
4065
+ return members.length === 1 ? members[0] : `z.union([${members.join(", ")}])`;
4066
+ }
4067
+ case "all": {
4068
+ const [first, ...rest] = value.parts.map((part) => mergedToZod(program, part));
4069
+ return rest.reduce((each, part) => `${each}.and(${part})`, first ?? UNREPRESENTABLE);
4070
+ }
4071
+ case "array": {
4072
+ const element = () => mergedToZod(program, value.element);
4073
+ return applyKeywords(program, `z.array(${value.list === undefined ? element() : atItem(program, value.list, element)})`, value.keywords, "array");
4074
+ }
4075
+ case "object":
4076
+ return objectToZod(program, value);
4077
+ }
4078
+ }
4079
+ /**
4080
+ * **The members a merged union is written with, by the validator and the contract type alike: one
4081
+ * of each text the validator writes, in an `anyOf`.** `M19 | M19` merged with `Record<M19>` was two
4082
+ * identical objects, whose two `z.input` types the wire assertion told apart from the one `Record` the
4083
+ * contract type is (`TS2344`). Each walk deduplicating by its own text disagreed the other way: `S3`
4084
+ * merged under `M1 | M2` is two objects the validator writes apart - `b` held to one `@minLength` and
4085
+ * to two - and the contract type wrote as one, and TypeScript counts a union of two such objects as
4086
+ * no object. So the validator decides, as it writes them, and the contract type writes the members
4087
+ * it kept - see {@link noteWrittenMembers}. Repeated in a `@oneOf`, a value matching both is refused,
4088
+ * so there each stays.
4089
+ */
4090
+ export function membersToWrite(program, value) {
4091
+ if (value.exclusive === true)
4092
+ return value.members;
4093
+ const kept = writtenMembers.get(program)?.get(mergedKeyOf(value));
4094
+ return kept === undefined ? value.members : kept.map((index) => value.members[index]);
4095
+ }
4096
+ /** The members of each merged union the validator wrote, by the union's {@link mergedKeyOf}. */
4097
+ const writtenMembers = new WeakMap();
4098
+ function noteWrittenMembers(program, value, kept) {
4099
+ let byKey = writtenMembers.get(program);
4100
+ if (byKey === undefined) {
4101
+ byKey = new Map();
4102
+ writtenMembers.set(program, byKey);
4103
+ }
4104
+ byKey.set(mergedKeyOf(value), kept);
4105
+ }
4106
+ /**
4107
+ * **A merged value's shape as text**, the same for the same value whichever walk merged it: each
4108
+ * walk merges a position afresh, so the two values are alike and never one object. A TypeSpec type is
4109
+ * itself, by an id of its own.
4110
+ */
4111
+ function mergedKeyOf(value) {
4112
+ if (value === null || typeof value !== "object")
4113
+ return JSON.stringify(value) ?? "undefined";
4114
+ if ("entityKind" in value) {
4115
+ let id = entityIds.get(value);
4116
+ if (id === undefined) {
4117
+ id = ++entityCount;
4118
+ entityIds.set(value, id);
4119
+ }
4120
+ return `#${id}`;
4121
+ }
4122
+ if (Array.isArray(value))
4123
+ return `[${value.map(mergedKeyOf).join(",")}]`;
4124
+ return `{${Object.keys(value)
4125
+ .toSorted()
4126
+ .map((key) => `${JSON.stringify(key)}:${mergedKeyOf(value[key])}`)
4127
+ .join(",")}}`;
4128
+ }
4129
+ const entityIds = new WeakMap();
4130
+ let entityCount = 0;
4131
+ /**
4132
+ * An object as Zod: its members, and what it does with a key none of them names.
4133
+ *
4134
+ * Both a model and a merge of several end here, so a merged object is written by the rules a model
4135
+ * is - and a model is just the merge of one.
4136
+ */
4137
+ function objectToZod(program, object) {
4138
+ const { rest } = object;
4139
+ const site = object.site;
4140
+ const restToZod = (value) => site === undefined
4141
+ ? mergedToZod(program, value)
4142
+ : withDeclarationSite(site, () => mergedToZod(program, value));
4143
+ /**
4144
+ * **A string indexer only means "dictionary" when the model declares NO properties.**
4145
+ *
4146
+ * `model X { id: string; ...Record<unknown> }` - the standard TypeSpec way to say "these fields,
4147
+ * plus anything else", and what `@loose` was replaced by - carries both. Returning
4148
+ * `z.record(...)` for it emits a schema that accepts an object and then **strips every declared
4149
+ * field**, so `respond()`'s drift detection degrades to "is it an object" and the pinned fields
4150
+ * silently stop being checked. This function's own history is the other half of the same mistake:
4151
+ * it once keyed on the model's *name*, and lost the indexer of anything derived from `Record`.
4152
+ */
4153
+ if (object.dictionary && typeof rest !== "string") {
4154
+ return applyKeywords(program, `z.record(z.string(), ${restToZod(rest)})`, object.keywords, "object");
4155
+ }
4156
+ if (object.dictionary && rest === "loose") {
4157
+ return applyKeywords(program, "z.record(z.string(), z.unknown())", object.keywords, "object");
1165
4158
  }
1166
4159
  /**
1167
- * **`rfc3339` on a date-time is the form it already has, so it changes nothing.** openapi3's
1168
- * `mergeFormatAndEncoding` keeps `format: date-time` for it, and the type it reports, `string`,
1169
- * emitted `z.string()`: the explicit spelling of the default dropped the date-time check the
1170
- * document still states. Only the encodings that change the wire form - `rfc7231`,
1171
- * `unixTimestamp` - change the type.
4160
+ * Permissiveness, read from the INDEXER and from nothing else.
4161
+ *
4162
+ * `...Record<unknown>` is the standard TypeSpec construct and the one `@typespec/openapi3` can
4163
+ * see, so it is the only source. There was a second - `@loose`, a decorator of ours, invisible to
4164
+ * the document - and it is gone: a model that accepted unknown properties while the published
4165
+ * schema said otherwise is a runtime disagreeing with its own contract.
4166
+ *
4167
+ * An indexer over something other than `unknown` is a typed catchall, where `.loose()` would drop
4168
+ * that value type on the floor.
4169
+ *
4170
+ * **`Record<never>` is how a spec says "and nothing else", not a value type to walk** - sealed,
4171
+ * as `@typespec/openapi3` publishes it with `additionalProperties: {not: {}}`.
4172
+ *
4173
+ * A request model REJECTS an unrecognised field rather than stripping it. Silently dropping
4174
+ * something a caller sent is the same failure this whole surface exists to remove, one layer out:
4175
+ * the request succeeds and does less than it was asked to, with nothing anywhere reporting it.
1172
4176
  */
1173
- if (encoded.encoding === "rfc3339" && isDateTime(program, target))
1174
- return undefined;
1175
- if (target.kind === "ModelProperty" &&
1176
- isQueryParam(program, target) &&
1177
- PARAMETER_STYLE_ENCODINGS.has(encoded.encoding ?? "")) {
1178
- return undefined;
1179
- }
1180
- return encoded.type;
4177
+ const catchall = typeof rest === "string" ? undefined : rest;
4178
+ /**
4179
+ * **A property that refers back into an unfinished declaration is emitted as a GETTER.**
4180
+ *
4181
+ * `model InnerModel { children?: InnerModel[] }` is a plain recursive tree, and openapi3 publishes
4182
+ * it as a self-`$ref`. Emitting `children: z.array(innerModelSchema)` reads the `const` inside its
4183
+ * own initialiser, so the module throws on import; deferring the reference behind a getter is the
4184
+ * idiom Zod documents for exactly this, and it is the only one that keeps `z.infer` inferring the
4185
+ * recursive type rather than collapsing to `any`. `z.lazy(() => innerModelSchema)` runs correctly
4186
+ * but does not typecheck - measured: `TS7022`, "implicitly has type 'any' because it does not have
4187
+ * a type annotation and is referenced directly or indirectly in its own initializer".
4188
+ */
4189
+ const members = object.members.map((member) => {
4190
+ const key = objectKey(member.key);
4191
+ const { property, position } = member;
4192
+ if (property === undefined) {
4193
+ const value = captureBackEdges(() => mergedToZod(program, position.value ?? NOTHING));
4194
+ // Optional where no declaration requires it - an envelope's members under a PATCH.
4195
+ const schema = position.required ? value.value : `${value.value}.exactOptional()`;
4196
+ return {
4197
+ deferred: value.deferred,
4198
+ text: value.deferred
4199
+ ? `\tget ${key}() {\n\t\treturn ${schema};\n\t},`
4200
+ : `\t${key}: ${schema},`,
4201
+ };
4202
+ }
4203
+ const { value, deferred } = captureBackEdges(() => withDeclarationSite(property, () => atPosition(position, () => atDeclaration(program, property, () => propertyToZod(program, property, position)))));
4204
+ /**
4205
+ * **A getter without a return type infers `unknown`, and the collapse is SILENT.**
4206
+ *
4207
+ * The docblock above claims the getter keeps `z.infer` inferring the recursive type. It does
4208
+ * for the declaration itself, and not for a schema that WRAPS it: measured on the conformance
4209
+ * corpus, `z.record(z.string(), innerModelSchema)` - what `@body body: Record<InnerModel>`
4210
+ * emits - infers `children?: unknown`, and `.parse()` on it returns `unknown` outright. The
4211
+ * generated server then failed to compile with `TS2322`, in `type/dictionary`, which is how
4212
+ * this was found; nothing said the emitted TYPE was weaker than the document.
4213
+ *
4214
+ * Annotating the GETTER rather than the declaration is what keeps `.shape` and `.extend`,
4215
+ * which inheritance and every group spread depend on. `: z.ZodType<T, T>` names both of Zod's
4216
+ * parameters for the reason `renderSchemas` names both: one leaves the input side `unknown`.
4217
+ */
4218
+ const annotation = deferred
4219
+ ? (atDeclaration(program, property, () => annotateDeferred(property, position)) ?? "")
4220
+ : "";
4221
+ return {
4222
+ deferred,
4223
+ text: deferred
4224
+ ? `\tget ${key}()${annotation} {\n\t\treturn ${value};\n\t},`
4225
+ : `\t${key}: ${value},`,
4226
+ };
4227
+ });
4228
+ const entries = members.map((member) => member.text);
4229
+ const suffix = rest === "loose"
4230
+ ? ".loose()"
4231
+ : catchall === undefined
4232
+ ? ""
4233
+ : `.catchall(${restToZod(catchall)})`;
4234
+ const strict = rest === "sealed" ? ".strict()" : "";
4235
+ /**
4236
+ * **A deferred property forces the CONSTRUCTOR form of the object.**
4237
+ *
4238
+ * Measured on Zod 4.4.3: `.strict()`, `.loose()` and `.catchall()` all read `shape` eagerly, so
4239
+ * each one fires the getter during module initialisation and throws `Cannot access 'X' before
4240
+ * initialization` - the very thing the getter exists to avoid. `z.strictObject` and
4241
+ * `z.looseObject` take the same shape without reading it.
4242
+ *
4243
+ * **So the constructor form is used wherever it exists, not only where a cycle forces it.** It is
4244
+ * also what Zod 4 asks for: its own types say "Consider `z.looseObject(A.shape)` instead" on
4245
+ * `.loose()` and `.strict()`, and `.passthrough()` is deprecated outright. Emitting one spelling
4246
+ * means a model that gains a back edge later does not silently change form, and the eager-`shape`
4247
+ * hazard above stops being something this walk has to keep reasoning about.
4248
+ *
4249
+ * **A TYPED catchall has no constructor form, and that used to be a refusal.** There is nowhere
4250
+ * safe to put the getter - `.catchall()` reads `shape` eagerly - so the cycle was reported as
4251
+ * `circular-model`. `z.lazy()` is the place to put it: wrapping the whole expression defers the
4252
+ * getter past module initialisation, and by the time the schema is forced every declaration exists.
4253
+ * Measured on 4.4.3: `z.lazy(() => z.object({ get kid() {...} }).catchall(z.number()))` parses a
4254
+ * nested value and still applies the catchall.
4255
+ */
4256
+ const lazily = members.some((member) => member.deferred);
4257
+ const constructible = suffix === "" || suffix === ".loose()";
4258
+ const shape = entries.length === 0 ? "{}" : `{\n${entries.join("\n")}\n}`;
4259
+ const body = constructible
4260
+ ? `${suffix === ".loose()" ? "z.looseObject" : strict === ".strict()" ? "z.strictObject" : "z.object"}(${shape})`
4261
+ : lazily
4262
+ ? `z.lazy(() => z.object(${shape})${suffix}${strict})`
4263
+ : `z.object(${shape})${suffix}${strict}`;
4264
+ /**
4265
+ * A model-level constraint goes through the same path as every other constraint. `.refine()`
4266
+ * returns an effect rather than an object, so applying it after `.loose()` is the only order that
4267
+ * works.
4268
+ */
4269
+ return applyKeywords(program, body, object.keywords, "object");
1181
4270
  }
1182
- export function propertyToZod(program, property) {
4271
+ /**
4272
+ * A property's VALUE as Zod - its type, encoding, constraints and nullability - without its
4273
+ * optionality, its default or its description, which belong to the position.
4274
+ */
4275
+ function propertyValueToZod(program, property, position = OWN_POSITION) {
1183
4276
  /**
1184
4277
  * **Constraints go INSIDE `.nullable()`, not after it.**
1185
4278
  *
@@ -1191,12 +4284,45 @@ export function propertyToZod(program, property) {
1191
4284
  * `.optional()` stays outermost: a constraint after it would apply to the optionality rather than
1192
4285
  * to the value.
1193
4286
  */
1194
- const nullable = nullableInnerOf(property.type);
1195
- // `@encode` on the property wins over its declared type - see `encodedTypeOf`. The nullable
1196
- // wrapper is kept: `@encode` changes what the value IS, not whether it may be absent.
1197
- const encoded = encodedTypeOf(program, property);
1198
- const declared = encoded ?? nullable ?? property.type;
1199
- const bare = nullable === undefined || encoded !== undefined;
4287
+ /**
4288
+ * `@encode` is read as `encodedValueOf` says the document publishes it, nullability included:
4289
+ * openapi3 3.1 publishes `anyOf: [<the encoded value>, { type: "null" }]` where the encoding is
4290
+ * applied beside `null`, and the encoded `type` rules `null` out where it is applied to the whole.
4291
+ */
4292
+ const encoding = encodedValueOf(program, property);
4293
+ /**
4294
+ * **A named union is its own declaration**, where nothing about the position is applied to its
4295
+ * members: openapi3 publishes `m: MaybeN1<ItemN1>` as `$ref: MaybeN1`, and the contract types
4296
+ * declare `MaybeN1`. Peeled to `itemN1Schema.nullable()` here, it had no validator.
4297
+ */
4298
+ const whole = property.type.kind === "Union" &&
4299
+ !shouldInline(program, property.type) &&
4300
+ encoding.kind === "declared" &&
4301
+ !position.nonNull &&
4302
+ position.constrainedBy.length === 0 &&
4303
+ !hasConstraints(program, property);
4304
+ const nullable = whole ? undefined : peeledNullOf(program, property);
4305
+ if (encoding.kind === "unsatisfiable") {
4306
+ reportUnsatisfiableEncoding(program, property, {
4307
+ name: property.name,
4308
+ declared: nameOfType(encoding.declared),
4309
+ encoded: encoding.scalar.name,
4310
+ why: encoding.why,
4311
+ });
4312
+ }
4313
+ // A declaration above refusing `null` leaves the value without it - see `PropertyPosition`.
4314
+ const bare = position.nonNull ||
4315
+ (encoding.kind === "declared"
4316
+ ? nullable === undefined
4317
+ : encoding.kind === "unsatisfiable" || !encoding.nullable);
4318
+ /** The type whose schema is the value, for the description a scalar carries. */
4319
+ const declared = encoding.kind === "declared"
4320
+ ? (nullable ?? property.type)
4321
+ : encoding.kind === "replaced"
4322
+ ? encoding.scalar
4323
+ : encoding.kind === "both" && !Array.isArray(encoding.kept)
4324
+ ? encoding.kept
4325
+ : property.type;
1200
4326
  /**
1201
4327
  * **One description per position, and a property's own doc wins over its scalar's.**
1202
4328
  *
@@ -1212,10 +4338,60 @@ export function propertyToZod(program, property) {
1212
4338
  * branch and only the property's belongs to the property.
1213
4339
  */
1214
4340
  const inlinedScalar = bare && declared.kind === "Scalar";
1215
- const value = inlinedScalar ? scalarSchemaOf(program, declared) : typeToZod(program, declared);
1216
- const constrained = applyConstraints(program, value, property);
4341
+ // A property's `@oneOf` makes its INLINE union exclusive; a named one is a `$ref` it leaves alone.
4342
+ const exclusive = exclusiveSiteOf(program, property, declared);
4343
+ const schemaOf = (type) => bare && type.kind === "Scalar"
4344
+ ? scalarSchemaOf(program, type)
4345
+ : withExclusiveSite(exclusive, () => typeToZod(program, type));
4346
+ let constrained;
4347
+ switch (encoding.kind) {
4348
+ case "unsatisfiable":
4349
+ constrained = UNREPRESENTABLE;
4350
+ break;
4351
+ case "declared": {
4352
+ const sources = [property, ...position.constrainedBy];
4353
+ const members = constrainedMembersOf(program, property, position);
4354
+ constrained =
4355
+ members === undefined
4356
+ ? withAncestors(program, applyConstraints(program, schemaOf(declared), property), property, position)
4357
+ : members.length === 0
4358
+ ? UNREPRESENTABLE
4359
+ : membersToZod(program, members, (member) => constrainedMemberToZod(program, member, sources), exclusive !== undefined || isExclusive(program, declared));
4360
+ break;
4361
+ }
4362
+ case "replaced":
4363
+ constrained = withAncestors(program, applyConstraints(program, schemaOf(encoding.scalar), property), property, position);
4364
+ break;
4365
+ case "both": {
4366
+ /**
4367
+ * The property's own constraints are applied to the encoded scalar's schema, never to an
4368
+ * intersection, which carries no `.min` or `.regex`; the kept type brings its own.
4369
+ */
4370
+ const kept = keptToZod(program, encoding, schemaOf);
4371
+ constrained = !encodedIntersects(program, property, encoding)
4372
+ ? applyConstraints(program, kept, property)
4373
+ : `${applyConstraints(program, scalarSchemaOf(program, encoding.scalar), property)}.and(${kept})`;
4374
+ break;
4375
+ }
4376
+ }
1217
4377
  const base = bare ? constrained : `${constrained}.nullable()`;
1218
4378
  const description = getDoc(program, property) ?? (inlinedScalar ? scalarDocOf(program, declared) : undefined);
4379
+ return { base, description };
4380
+ }
4381
+ export function propertyToZod(program, property, position = OWN_POSITION) {
4382
+ if (position.refused !== undefined)
4383
+ reportUnrepresentableOverride(program, property, position);
4384
+ // Merged with the declarations above it where the value narrows none - see `mergeParts`.
4385
+ const own = position.value === undefined
4386
+ ? atReading(program, property, () => propertyValueToZod(program, property, position))
4387
+ : { base: mergedToZod(program, position.value), description: getDoc(program, property) };
4388
+ // What the declarations above it leave of the value - see `PropertyPosition`.
4389
+ const base = position.never || position.refused !== undefined
4390
+ ? UNREPRESENTABLE
4391
+ : position.nullOnly
4392
+ ? "z.null()"
4393
+ : own.base;
4394
+ const { description } = own;
1219
4395
  /**
1220
4396
  * **Optionality is a function of the position too, not only of the `?` in the spec.**
1221
4397
  *
@@ -1225,8 +4401,12 @@ export function propertyToZod(program, property) {
1225
4401
  * would reject every partial update the contract invites. `metadataInfo.isOptional` is the same
1226
4402
  * predicate `@typespec/openapi3` uses to decide the `required` list.
1227
4403
  */
1228
- const optional = property.optional || isOptionalAt(program, property);
4404
+ const optional = !position.required && (property.optional || isOptionalAt(program, property));
1229
4405
  reportInheritedName(program, property, optional);
4406
+ // A value nothing satisfies takes no default: there is no value to fall back to.
4407
+ if (refusedValue(program, property, position)) {
4408
+ return described(optional ? `${base}.exactOptional()` : base, description);
4409
+ }
1230
4410
  /**
1231
4411
  * **A default only applies where the document says the property may be absent.**
1232
4412
  *
@@ -1243,11 +4423,19 @@ export function propertyToZod(program, property) {
1243
4423
  * about a value that cannot be reached either way, naming the less useful of the two problems.
1244
4424
  */
1245
4425
  if (!optional && property.defaultValue !== undefined) {
1246
- reportDiagnostic(program, {
1247
- code: "default-on-required-property",
1248
- target: property,
1249
- format: { name: property.name },
1250
- });
4426
+ // Required by its position rather than its `?`: a discriminator, or a declaration above it.
4427
+ const imposed = position.required && (property.optional || isOptionalAt(program, property));
4428
+ const discriminator = imposed &&
4429
+ property.model !== undefined &&
4430
+ discriminatorsOf(program, property.model).includes(rawPropertyKey(program, property));
4431
+ if (firstReportOf(program, "default-on-required-property", property)) {
4432
+ reportDiagnostic(program, {
4433
+ code: "default-on-required-property",
4434
+ messageId: discriminator ? "discriminator" : imposed ? "ancestor" : "default",
4435
+ target: property,
4436
+ format: { name: property.name },
4437
+ });
4438
+ }
1251
4439
  return described(base, description);
1252
4440
  }
1253
4441
  /**
@@ -1257,9 +4445,19 @@ export function propertyToZod(program, property) {
1257
4445
  * already accepts an absent value, and the two together would make the OUTPUT optional as well,
1258
4446
  * so the fallback could be skipped.
1259
4447
  */
1260
- const defaulted = defaultOf(program, property);
1261
- if (defaulted !== undefined)
1262
- return described(`${base}.default(${defaulted})`, description);
4448
+ const defaulted = defaultOf(program, property, position);
4449
+ if (defaulted !== undefined) {
4450
+ /**
4451
+ * **A default no literal of the validator's type spells is the validator's own parse of it** -
4452
+ * see {@link setsKeyItsIndexerRefuses}. The value is the same: it was held to the schema above,
4453
+ * and a parse of a valid value is that value with its defaults filled, as the literal is.
4454
+ */
4455
+ const value = jsonLiteral(defaulted);
4456
+ const fallback = setsKeyItsIndexerRefuses(program, property.type, defaulted)
4457
+ ? `() => ${base}.parse(${value})`
4458
+ : value;
4459
+ return described(`${base}.default(${fallback})`, description);
4460
+ }
1263
4461
  /**
1264
4462
  * **`.exactOptional()`, because the document says the KEY may be absent and nothing more.**
1265
4463
  *
@@ -1339,58 +4537,6 @@ function wireNameOf(program, property) {
1339
4537
  }
1340
4538
  return rawPropertyKey(program, property);
1341
4539
  }
1342
- /**
1343
- * A default value as a JS literal, or `undefined` where this emitter cannot render one.
1344
- *
1345
- * **Composite defaults used to be REFUSED, and the refusal was wrong on the emitter's own
1346
- * governing rule.** `#["a", "b"]` and `#{ x: 1 }` were reported as `unsupported-default` with the
1347
- * reason "a populated literal default would need each element rendered, and no schema here has one".
1348
- * That was a statement about this function rather than about what can be represented: `.default()`
1349
- * takes any JS value, and `@typespec/openapi3` publishes these exactly -
1350
- * `default: ["a","b"]`, `default: {"x":1,"label":"hi"}`, and nested arrays too, measured from one
1351
- * compile of a spec carrying all three. So the document could say it, Zod could enforce it, and only
1352
- * this emitter refused - which is the same spec being representable by one emitter and not the other,
1353
- * the one thing a differential between them cannot tolerate.
1354
- *
1355
- * **The refusal also emitted `.default(z.never())`**, because `UNREPRESENTABLE` is a schema
1356
- * expression and this position wants a VALUE. Had the diagnostic ever been downgraded to a warning,
1357
- * that would have compiled and set every such default to a Zod object.
1358
- *
1359
- * A value is pure data - strings, numbers, booleans, null, and arrays and objects of those - so it
1360
- * renders as JSON. Anything else stays a refusal, and `ScalarValue` is the live case: a default like
1361
- * `utcDateTime.fromISO(...)` is a constructor call rather than a literal.
1362
- */
1363
- function renderValue(value) {
1364
- if (value.valueKind === "StringValue")
1365
- return literal(value.value);
1366
- if (value.valueKind === "NumericValue")
1367
- return String(value.value.asNumber());
1368
- if (value.valueKind === "BooleanValue")
1369
- return String(value.value);
1370
- if (value.valueKind === "NullValue")
1371
- return "null";
1372
- if (value.valueKind === "EnumValue") {
1373
- const member = value.value;
1374
- return literal(member.value ?? member.name);
1375
- }
1376
- if (value.valueKind === "ArrayValue") {
1377
- const rendered = value.values.map(renderValue);
1378
- // One unrenderable element makes the whole literal unrenderable - a partial array would be a
1379
- // different default from the one the spec declares, which is worse than refusing.
1380
- return rendered.some((entry) => entry === undefined) ? undefined : `[${rendered.join(", ")}]`;
1381
- }
1382
- if (value.valueKind === "ObjectValue") {
1383
- const entries = [];
1384
- for (const [name, descriptor] of value.properties) {
1385
- const rendered = renderValue(descriptor.value);
1386
- if (rendered === undefined)
1387
- return undefined;
1388
- entries.push(`${literal(name)}: ${rendered}`);
1389
- }
1390
- return `{${entries.join(", ")}}`;
1391
- }
1392
- return undefined;
1393
- }
1394
4540
  /**
1395
4541
  * **Whether `propertyToZod` emits `.default(...)` for this property, which is exactly whether its
1396
4542
  * INPUT admits an explicit `undefined`.**
@@ -1403,34 +4549,483 @@ function renderValue(value) {
1403
4549
  *
1404
4550
  * Pure: it reports nothing, so the type walk can ask it without duplicating a diagnostic.
1405
4551
  */
1406
- export function emitsDefault(program, property) {
1407
- const optional = property.optional || isOptionalAt(program, property);
1408
- return (optional &&
1409
- property.defaultValue !== undefined &&
1410
- renderValue(property.defaultValue) !== undefined);
4552
+ export function emitsDefault(program, property, position = OWN_POSITION) {
4553
+ const optional = !position.required && (property.optional || isOptionalAt(program, property));
4554
+ if (!optional ||
4555
+ property.defaultValue === undefined ||
4556
+ refusedValue(program, property, position)) {
4557
+ return false;
4558
+ }
4559
+ const serialized = defaultJsonOf(program, property);
4560
+ return (serialized !== undefined &&
4561
+ defaultClashOf(program, property, position, serialized.json) === undefined);
4562
+ }
4563
+ /**
4564
+ * Whether the value at this position validates nothing - refused, or no value of it is valid - so
4565
+ * it takes no default: there is nothing to fall back to, and `z.never().default(...)` does not
4566
+ * compile.
4567
+ */
4568
+ function refusedValue(program, property, position) {
4569
+ if (position.never || position.nullOnly || position.refused !== undefined)
4570
+ return true;
4571
+ if (encodedValueOf(program, property).kind === "unsatisfiable")
4572
+ return true;
4573
+ const declared = nullableInnerOf(property.type) ?? property.type;
4574
+ // A tuple parameter is refused as `unsupported-type`.
4575
+ return declared.kind === "Tuple" && parameterPosition;
4576
+ }
4577
+ /**
4578
+ * **Why a default is no value the position accepts, or `undefined` where it is one.**
4579
+ *
4580
+ * A default is written in the declared type and published as the JSON openapi3 serializes it, and
4581
+ * that is not always a value of the schema it sits beside. `@encode` can change the type under it -
4582
+ * `@encode(string) count?: int32 = 5` is `{ type: "string", default: 5 }`, and a scalar built on an
4583
+ * encoded scalar carries the encoding too; a property's constraints can rule out a union's literal
4584
+ * the default names; a key the default sets can be one the position does not publish - a read-only
4585
+ * property in a create body, a model the document only knows as a union member. Filled in, any of
4586
+ * these handed the handler a value its type says it cannot hold, and the emitted module did not
4587
+ * compile. No fallback is both the document's and a valid value, so none is emitted, and
4588
+ * `invalid-default` says why. Checked member by member through lists, records and objects.
4589
+ */
4590
+ function defaultClashOf(program, property, position, json) {
4591
+ /**
4592
+ * **An integer past what a JSON number holds exactly is published as `null`** - `int64 = 2^53 + 1`
4593
+ * - and was reported as though the author had written `null`.
4594
+ */
4595
+ const written = property.defaultValue;
4596
+ if (json === null && written?.valueKind === "NumericValue") {
4597
+ return `it is ${written.value.toString()}, which no JSON number holds exactly, and openapi3 publishes it as null`;
4598
+ }
4599
+ const clash = valueClashOf(program, property, position, json);
4600
+ /**
4601
+ * **Where openapi3 publishes fewer keys than the default sets, the message says so**: an object
4602
+ * default on a union-typed property is serialized as `{}`, and "it leaves out 'kind'" read as
4603
+ * though the spec had.
4604
+ */
4605
+ if (clash !== undefined &&
4606
+ written?.valueKind === "ObjectValue" &&
4607
+ json !== null &&
4608
+ typeof json === "object" &&
4609
+ Object.keys(json).length < written.properties.size) {
4610
+ return `openapi3 publishes it as ${JSON.stringify(json)}, without keys it sets, and ${clash}`;
4611
+ }
4612
+ return clash;
4613
+ }
4614
+ /**
4615
+ * **Why a JSON value is no value of a property at its position, or `undefined` where it is one** -
4616
+ * asked of a default, and of each key a default sets, at that key's own position.
4617
+ */
4618
+ function valueClashOf(program, property, position, json) {
4619
+ // Every declaration above holds the value too, merged or implied - a `null` one refuses included.
4620
+ for (const ancestor of position.ancestors) {
4621
+ const clash = jsonClashOf(program, json, valueJsonTypesOf(program, ancestor), ancestor.type);
4622
+ if (clash !== undefined)
4623
+ return clash;
4624
+ }
4625
+ /**
4626
+ * **The check its own scalar emits** - a width, a format - which the document publishes as
4627
+ * `format` and JSON Schema reads as an annotation, so no keyword above says it: a `plainDate` of
4628
+ * February 30th, a `url` that is no URL, a date past what an `int32` timestamp encodes. Each was
4629
+ * filled in, and refused when sent back. Asked of the encoded scalar where `@encode` replaces the
4630
+ * value's type, as the validator asks it - and first, since a keyword `@encode` moves onto a type it
4631
+ * does not apply to is dropped: `@encode(string) @minValue(5) low?: int32 = 3` is no string, and
4632
+ * was reported as breaking the `@minValue` the validator does not enforce.
4633
+ */
4634
+ const declared = nullableInnerOf(property.type) ?? property.type;
4635
+ const encoding = encodedValueOf(program, property);
4636
+ const validated = encoding.kind === "replaced" || encoding.kind === "both"
4637
+ ? encoding.scalar
4638
+ : declared.kind === "Scalar"
4639
+ ? declared
4640
+ : undefined;
4641
+ if (validated !== undefined &&
4642
+ json !== null &&
4643
+ scalarAccepts(program, validated, json) === false) {
4644
+ return `it is no '${validated.name}' the validator accepts`;
4645
+ }
4646
+ /**
4647
+ * **And every keyword on the way: the property's, each declaration's above it, and its declared
4648
+ * scalar's.** `@minValue(10) mv?: int32 = 5` filled a `5` the same validator refuses when it is
4649
+ * sent, and the handler was handed it.
4650
+ */
4651
+ const broken = keywordClashOf(program, json, [
4652
+ property,
4653
+ ...position.constrainedBy,
4654
+ ...position.ancestors,
4655
+ ...(declared.kind === "Scalar" ? [declared] : []),
4656
+ ]);
4657
+ if (broken !== undefined)
4658
+ return `it breaks ${broken}`;
4659
+ /**
4660
+ * **A merged value is the one schema the document holds it to**, so it is asked whole: what no
4661
+ * declaration states is part of it - the value of a typed indexer of a model merged with this
4662
+ * one, which holds every key that model does not name. `Root { u: B | A }` with `A` a
4663
+ * `Record<Small>` and `Sub { u: B2 }` filled `B2`'s `k = 7` into the `A` branch, where `Small`
4664
+ * refuses it.
4665
+ */
4666
+ if (position.value !== undefined) {
4667
+ const clash = mergedClashOf(program, json, position.value);
4668
+ if (clash !== undefined)
4669
+ return clash;
4670
+ }
4671
+ const members = position.value === undefined ? constrainedMembersOf(program, property, position) : undefined;
4672
+ if (members !== undefined) {
4673
+ const fits = (json === null && hasNull(property.type)) ||
4674
+ members.some((member) => jsonClashOf(program, json, jsonTypesOf(program, member), member) === undefined);
4675
+ return fits ? undefined : "it is no value of the members the property's constraints leave";
4676
+ }
4677
+ return jsonClashOf(program, json, valueJsonTypesOf(program, property), property.type);
4678
+ }
4679
+ /** The first keyword of these declarations a JSON value breaks, for its own JSON type. */
4680
+ function keywordClashOf(program, json, sources) {
4681
+ for (const source of sources) {
4682
+ const broken = typeof json === "string" || typeof json === "number"
4683
+ ? brokenConstraintOf(program, source, json)
4684
+ : Array.isArray(json)
4685
+ ? brokenItemsOf(program, source, json.length)
4686
+ : undefined;
4687
+ if (broken !== undefined)
4688
+ return broken;
4689
+ }
4690
+ return undefined;
4691
+ }
4692
+ /** Why a JSON value does not fit a declared type, by kind and then member by member, or `undefined`. */
4693
+ function jsonClashOf(program, json, types, type) {
4694
+ const kind = jsonTypeOfValue(json);
4695
+ const fits = types === undefined ||
4696
+ types.has(kind) ||
4697
+ (kind === "integer" && types.has("number")) ||
4698
+ (kind === "number" && types.has("fraction"));
4699
+ if (!fits) {
4700
+ const expected = [...(types ?? [])].filter((each) => each !== "null").join(" or ");
4701
+ return `it is a JSON ${kind}, and the value is a JSON ${expected}`;
4702
+ }
4703
+ const declared = nullableInnerOf(type) ?? type;
4704
+ /**
4705
+ * **A literal admits its own value and no other, and an enum or a union of literals its members'.**
4706
+ * Read by JSON type alone, a default a literal above refuses was filled in: `Leaf { kk?: string =
4707
+ * "abcd" }` named `k` with `@encodedName` over `Root { k?: "b" }`, which the compiler does not hold
4708
+ * `kk` to, and `M14 { a?: string = "a" }` merged into `Record<Color.red>`, were emitted as
4709
+ * `z.literal("b").default("abcd")`, refused when sent back, and the module failed to typecheck.
4710
+ */
4711
+ const fixed = literalValuesOfType(declared);
4712
+ if (fixed !== undefined &&
4713
+ (typeof json === "string" || typeof json === "number" || typeof json === "boolean") &&
4714
+ !admitsValue(program, declared, json)) {
4715
+ return `it is ${JSON.stringify(json)}, and the value is ${fixed.map((value) => JSON.stringify(value)).join(" or ")}`;
4716
+ }
4717
+ // A declared scalar's keywords, and a named list model's, hold at every depth.
4718
+ if (declared.kind === "Scalar" || declared.kind === "Model") {
4719
+ const broken = keywordClashOf(program, json, [declared]);
4720
+ if (broken !== undefined)
4721
+ return `it breaks ${broken}`;
4722
+ }
4723
+ if (declared.kind === "Union" && kind !== "null") {
4724
+ const members = membersOf(declared).filter((member) => !isNull(member));
4725
+ const clashes = members.map((member) => jsonClashOf(program, json, jsonTypesOf(program, member), member));
4726
+ return clashes.includes(undefined) ? undefined : clashes[0];
4727
+ }
4728
+ if (declared.kind !== "Model")
4729
+ return undefined;
4730
+ const element = declared.indexer?.key.name === "integer" ? declared.indexer.value : undefined;
4731
+ if (Array.isArray(json) && element !== undefined) {
4732
+ for (const item of json) {
4733
+ const clash = jsonClashOf(program, item, jsonTypesOf(program, element), element);
4734
+ if (clash !== undefined)
4735
+ return clash;
4736
+ }
4737
+ return undefined;
4738
+ }
4739
+ if (kind !== "object")
4740
+ return undefined;
4741
+ const entries = Object.entries(json);
4742
+ // A record of values, each checked as its value type.
4743
+ if (declared.indexer?.key.name === "string" && declared.properties.size === 0) {
4744
+ const value = declared.indexer.value;
4745
+ for (const [, each] of entries) {
4746
+ const clash = jsonClashOf(program, each, jsonTypesOf(program, value), value);
4747
+ if (clash !== undefined)
4748
+ return clash;
4749
+ }
4750
+ return undefined;
4751
+ }
4752
+ /**
4753
+ * **Each key at its own position, the declarations above it included**, read where the model is
4754
+ * rendered. Held to its own declaration alone, `o?: KS = #{ k: "toolong" }` was filled in where
4755
+ * `KS extends KB` and `KB` declares `@maxLength(3) k`: `KS`'s `allOf` refuses it.
4756
+ */
4757
+ return withVisibility(renderedVisibilityOf(program, declared, visibility), () => {
4758
+ const published = publishedPropertiesOf(program, declared).map((entry) => ({
4759
+ entry,
4760
+ key: rawPropertyKey(program, entry.property),
4761
+ position: positionOf(program, declared, entry),
4762
+ }));
4763
+ for (const [key, value] of entries) {
4764
+ const each = published.find((candidate) => candidate.key === key);
4765
+ if (each === undefined) {
4766
+ if (declared.indexer === undefined && isSealed(declared))
4767
+ return `it sets '${key}', which the value does not declare here`;
4768
+ continue;
4769
+ }
4770
+ const clash = valueClashOf(program, each.entry.property, each.position, value);
4771
+ if (clash !== undefined)
4772
+ return `at '${key}', ${clash}`;
4773
+ }
4774
+ const missing = published.find(({ entry, key, position }) => (position.required ||
4775
+ !(entry.property.optional || isOptionalAt(program, entry.property))) &&
4776
+ !entries.some(([sent]) => sent === key));
4777
+ return missing === undefined ? undefined : `it leaves out '${missing.key}', which is required`;
4778
+ });
4779
+ }
4780
+ /**
4781
+ * **Why a JSON value is no value of a merged schema**, read as the validator it renders reads it -
4782
+ * see {@link Merged}. A refusal is reported where it is found, so it clashes with nothing here.
4783
+ */
4784
+ function mergedClashOf(program, json, merged) {
4785
+ const kind = jsonTypeOfValue(json);
4786
+ switch (merged.kind) {
4787
+ case "never":
4788
+ return "no value is valid where it is filled in";
4789
+ case "refused":
4790
+ return undefined;
4791
+ case "type": {
4792
+ const clash = withVisibility(merged.at ?? visibility, () => jsonClashOf(program, json, jsonTypesOf(program, merged.type), merged.type));
4793
+ if (clash !== undefined)
4794
+ return clash;
4795
+ const broken = keywordClashOf(program, json, merged.keywords);
4796
+ return broken === undefined ? undefined : `it breaks ${broken}`;
4797
+ }
4798
+ case "whole":
4799
+ return valueClashOf(program, merged.property, OWN_POSITION, json);
4800
+ case "nullable":
4801
+ return json === null ? undefined : mergedClashOf(program, json, merged.inner);
4802
+ case "all":
4803
+ for (const part of merged.parts) {
4804
+ const clash = mergedClashOf(program, json, part);
4805
+ if (clash !== undefined)
4806
+ return clash;
4807
+ }
4808
+ return undefined;
4809
+ case "union": {
4810
+ const clashes = merged.members.map((member) => mergedClashOf(program, json, member));
4811
+ const fits = clashes.filter((clash) => clash === undefined).length;
4812
+ if (merged.exclusive === true ? fits === 1 : fits > 0)
4813
+ return undefined;
4814
+ return fits > 1
4815
+ ? "it is a value of more than one member of a '@oneOf' union"
4816
+ : (clashes[0] ?? "it is no value of the union");
4817
+ }
4818
+ case "array": {
4819
+ if (!Array.isArray(json))
4820
+ return `it is a JSON ${kind}, and the value is a JSON array`;
4821
+ const broken = keywordClashOf(program, json, merged.keywords);
4822
+ if (broken !== undefined)
4823
+ return `it breaks ${broken}`;
4824
+ for (const item of json) {
4825
+ const clash = mergedClashOf(program, item, merged.element);
4826
+ if (clash !== undefined)
4827
+ return clash;
4828
+ }
4829
+ return undefined;
4830
+ }
4831
+ case "object": {
4832
+ if (kind !== "object")
4833
+ return `it is a JSON ${kind}, and the value is a JSON object`;
4834
+ const entries = Object.entries(json);
4835
+ for (const [key, value] of entries) {
4836
+ const member = merged.members.find((each) => each.key === key);
4837
+ if (member === undefined) {
4838
+ if (merged.rest === "sealed") {
4839
+ return `it sets '${key}', which the value does not declare here`;
4840
+ }
4841
+ const clash = typeof merged.rest === "string"
4842
+ ? undefined
4843
+ : mergedClashOf(program, value, merged.rest);
4844
+ if (clash !== undefined)
4845
+ return `at '${key}', ${clash}`;
4846
+ continue;
4847
+ }
4848
+ const clash = member.property === undefined
4849
+ ? member.position.value === undefined
4850
+ ? undefined
4851
+ : mergedClashOf(program, value, member.position.value)
4852
+ : valueClashOf(program, member.property, member.position, value);
4853
+ if (clash !== undefined)
4854
+ return `at '${key}', ${clash}`;
4855
+ }
4856
+ // A merged member's position says whether any declaration of the key requires it.
4857
+ const missing = merged.members.find((member) => member.position.required && !entries.some(([sent]) => sent === member.key));
4858
+ return missing === undefined
4859
+ ? undefined
4860
+ : `it leaves out '${missing.key}', which is required`;
4861
+ }
4862
+ }
1411
4863
  }
1412
- /** A property's declared default, as a JS literal, or `undefined` when it has none. */
1413
- function defaultOf(program, property) {
4864
+ /**
4865
+ * **A default as the document publishes it**: openapi3's `serializeValueAsJson` - wire names for an
4866
+ * object's keys, a scalar constructor written through the property's encoding, so
4867
+ * `utcDateTime.fromISO("2020-01-01T00:00:00Z")` under `unixTimestamp` is `1577836800`. `undefined`
4868
+ * where it cannot be serialized, which openapi3 reports as `default-not-supported`.
4869
+ */
4870
+ function defaultJsonOf(program, property) {
1414
4871
  const value = property.defaultValue;
1415
4872
  if (value === undefined)
1416
4873
  return undefined;
1417
- const rendered = renderValue(value);
1418
- if (rendered !== undefined)
1419
- return rendered;
1420
- reportDiagnostic(program, {
1421
- code: "unsupported-default",
1422
- target: property,
1423
- format: { why: `kind "${value.valueKind}"` },
4874
+ try {
4875
+ const json = serializeValueAsJson(program, value, property);
4876
+ return json === undefined ? undefined : { json };
4877
+ }
4878
+ catch {
4879
+ return undefined;
4880
+ }
4881
+ }
4882
+ function jsonTypeOfValue(json) {
4883
+ if (json === null)
4884
+ return "null";
4885
+ if (Array.isArray(json))
4886
+ return "array";
4887
+ if (typeof json === "number")
4888
+ return Number.isInteger(json) ? "integer" : "number";
4889
+ return typeof json;
4890
+ }
4891
+ /** A JSON value as a JS literal, every string through `literal` so the module stays ASCII. */
4892
+ function jsonLiteral(json) {
4893
+ if (typeof json === "string")
4894
+ return literal(json);
4895
+ if (Array.isArray(json))
4896
+ return `[${json.map(jsonLiteral).join(", ")}]`;
4897
+ if (json !== null && typeof json === "object") {
4898
+ return `{${Object.entries(json)
4899
+ .map(([key, value]) => `${literal(key)}: ${jsonLiteral(value)}`)
4900
+ .join(", ")}}`;
4901
+ }
4902
+ return String(json);
4903
+ }
4904
+ /** A property's declared default, as the JSON the document publishes filled in, or `undefined`. */
4905
+ function defaultOf(program, property, position) {
4906
+ const value = property.defaultValue;
4907
+ if (value === undefined)
4908
+ return undefined;
4909
+ const serialized = defaultJsonOf(program, property);
4910
+ if (serialized === undefined) {
4911
+ reportDiagnostic(program, {
4912
+ code: "unsupported-default",
4913
+ target: property,
4914
+ format: { why: `kind "${value.valueKind}"` },
4915
+ });
4916
+ /**
4917
+ * **No `.default(...)` at all, rather than a default this emitter invented.** The property keeps
4918
+ * its declared shape and loses only the fallback, which the diagnostic names.
4919
+ */
4920
+ return undefined;
4921
+ }
4922
+ const clash = defaultClashOf(program, property, position, serialized.json);
4923
+ if (clash !== undefined) {
4924
+ if (firstReportOf(program, "invalid-default", property)) {
4925
+ reportDiagnostic(program, {
4926
+ code: "invalid-default",
4927
+ target: property,
4928
+ format: { name: property.name, why: clash },
4929
+ });
4930
+ }
4931
+ return undefined;
4932
+ }
4933
+ return filledDefaultOf(program, property.type, serialized.json);
4934
+ }
4935
+ /**
4936
+ * **Whether a default sets, at any depth, a declared key of an object whose typed indexer refuses the
4937
+ * key's value**, which no literal of the validator's type can do. Zod types `.catchall()` over every
4938
+ * key, declared ones included, so `ObjIdx { s?: string; ...Record<int32> }` is `{ [x: string]:
4939
+ * number; s?: string }`, and TypeScript holds `s` to both: `r?: ObjIdx = #{ s: "x" }`, which the
4940
+ * document and the validator accept, was emitted as `.default({"s": "x"})` and the module failed to
4941
+ * typecheck, `TS2769`, the default no value of the type. A key the indexer holds as well, `#{ k: 1 }`,
4942
+ * is one.
4943
+ */
4944
+ function setsKeyItsIndexerRefuses(program, type, json) {
4945
+ const declared = nullableInnerOf(type) ?? type;
4946
+ if (json === null || typeof json !== "object")
4947
+ return false;
4948
+ if (declared.kind === "Union") {
4949
+ return membersOf(declared).some((member) => setsKeyItsIndexerRefuses(program, member, json));
4950
+ }
4951
+ if (declared.kind !== "Model")
4952
+ return false;
4953
+ if (Array.isArray(json)) {
4954
+ const element = declared.indexer?.key.name === "integer" ? declared.indexer.value : undefined;
4955
+ return (element !== undefined &&
4956
+ json.some((item) => setsKeyItsIndexerRefuses(program, element, item)));
4957
+ }
4958
+ return withVisibility(renderedVisibilityOf(program, declared, visibility), () => {
4959
+ const object = objectOfModel(program, declared);
4960
+ if (object.kind !== "object")
4961
+ return false;
4962
+ const rest = typeof object.rest === "string" ? undefined : object.rest;
4963
+ return Object.entries(json).some(([key, value]) => {
4964
+ const member = object.members.find((each) => each.key === key);
4965
+ if (member === undefined || object.dictionary) {
4966
+ return rest?.kind === "type" && setsKeyItsIndexerRefuses(program, rest.type, value);
4967
+ }
4968
+ if (rest !== undefined && mergedClashOf(program, value, rest) !== undefined)
4969
+ return true;
4970
+ return (member.property !== undefined &&
4971
+ setsKeyItsIndexerRefuses(program, member.property.type, value));
4972
+ });
4973
+ });
4974
+ }
4975
+ /**
4976
+ * **A default as the validator hands it over: the document's value, with the default of every key it
4977
+ * leaves out filled in, at any depth.** `.default()` hands its value over unparsed, so
4978
+ * `inner?: Inner = #{ b: "x" }` beside `Inner`'s own `a?: int32 = 1` gave the handler an `Inner` with
4979
+ * no `a`, and the module did not typecheck: the value is the validator's OUTPUT, where `a` is filled.
4980
+ * Filled here, it is the value a parse of the document's default produces.
4981
+ */
4982
+ function filledDefaultOf(program, type, json) {
4983
+ const declared = nullableInnerOf(type) ?? type;
4984
+ if (declared.kind !== "Model" || json === null || typeof json !== "object")
4985
+ return json;
4986
+ const indexed = declared.indexer;
4987
+ if (Array.isArray(json)) {
4988
+ return indexed?.key.name === "integer"
4989
+ ? json.map((item) => filledDefaultOf(program, indexed.value, item))
4990
+ : json;
4991
+ }
4992
+ const own = (target, key, value) => {
4993
+ // Defined as an own key, so a key named `__proto__` is a key and not the prototype.
4994
+ Object.defineProperty(target, key, {
4995
+ value,
4996
+ enumerable: true,
4997
+ writable: true,
4998
+ configurable: true,
4999
+ });
5000
+ };
5001
+ const filled = {};
5002
+ if (indexed?.key.name === "string" && declared.properties.size === 0) {
5003
+ for (const [key, value] of Object.entries(json)) {
5004
+ own(filled, key, filledDefaultOf(program, indexed.value, value));
5005
+ }
5006
+ return filled;
5007
+ }
5008
+ for (const [key, value] of Object.entries(json))
5009
+ own(filled, key, value);
5010
+ return withVisibility(renderedVisibilityOf(program, declared, visibility), () => {
5011
+ for (const entry of publishedPropertiesOf(program, declared)) {
5012
+ const key = rawPropertyKey(program, entry.property);
5013
+ if (Object.prototype.hasOwnProperty.call(filled, key)) {
5014
+ own(filled, key, filledDefaultOf(program, entry.property.type, filled[key]));
5015
+ continue;
5016
+ }
5017
+ if (!emitsDefault(program, entry.property, positionOf(program, declared, entry)))
5018
+ continue;
5019
+ const inner = defaultJsonOf(program, entry.property);
5020
+ if (inner !== undefined) {
5021
+ own(filled, key, filledDefaultOf(program, entry.property.type, inner.json));
5022
+ }
5023
+ }
5024
+ return filled;
1424
5025
  });
1425
- /**
1426
- * **No `.default(...)` at all, rather than a default this emitter invented.** Returning
1427
- * `UNREPRESENTABLE` here put a schema expression where a value belongs. The property keeps its
1428
- * declared shape and loses only the fallback, which the diagnostic names.
1429
- */
1430
- return undefined;
1431
5026
  }
1432
5027
  /** The non-null half of a two-variant `T | null` union, or `undefined` if it is not one. */
1433
- function nullableInnerOf(type) {
5028
+ export function nullableInnerOf(type) {
1434
5029
  if (type.kind !== "Union")
1435
5030
  return undefined;
1436
5031
  const variants = [...type.variants.values()].map((variant) => variant.type);
@@ -1501,12 +5096,31 @@ export function typeToZod(program, type) {
1501
5096
  * occurrences still go through {@link typeToZod} and do resolve - which is what makes a recursive
1502
5097
  * model reference itself instead of inlining itself forever.
1503
5098
  */
5099
+ /**
5100
+ * **An instance of a list template is refused**: `model Page<T> is T[]` made `Page<Item>`, which
5101
+ * openapi3 emits as a template instance - an object - and publishes as `type: object` with the
5102
+ * element as `additionalProperties`, a dictionary where the spec declares a list. No validator agrees
5103
+ * with both. A named list per element type, `model Items is Item[]`, or `Item[]` itself, is a list in
5104
+ * the document too.
5105
+ */
5106
+ export function listTemplateInstanceWhy(program, type) {
5107
+ return type.templateMapper !== undefined &&
5108
+ type.indexer?.key.name === "integer" &&
5109
+ !isArrayLiteral(program, type)
5110
+ ? "an instance of a list template, which openapi3 publishes as an object of its elements, not a list"
5111
+ : undefined;
5112
+ }
5113
+ export const LIST_TEMPLATE_REMEDY = "Declare a named list model per element type, `model Items is Item[]`, or use `Item[]` itself.";
1504
5114
  export function typeToZodBody(program, type) {
1505
5115
  switch (type.kind) {
1506
5116
  case "Scalar":
1507
5117
  return scalarToZod(program, type);
1508
- case "Model":
5118
+ case "Model": {
5119
+ const why = listTemplateInstanceWhy(program, type);
5120
+ if (why !== undefined)
5121
+ return refuse(program, type, why, LIST_TEMPLATE_REMEDY);
1509
5122
  return modelToZod(program, type);
5123
+ }
1510
5124
  case "Enum":
1511
5125
  return enumToZod(program, type);
1512
5126
  case "Union":
@@ -1536,9 +5150,26 @@ export function typeToZodBody(program, type) {
1536
5150
  if (type.name === "unknown")
1537
5151
  return "z.unknown()";
1538
5152
  if (type.name === "void" || type.name === "never") {
1539
- return refuse(program, type, `\`${type.name}\` has no runtime representation`, "Give the position a type a value can inhabit. A `never` PROPERTY is dropped to match the document, and `Record<never>` seals the model, so neither of those reaches here.");
5153
+ return refuse(program, type, `\`${type.name}\` has no runtime representation`, "Give the position a type a value can inhabit. A `never` property or parameter is dropped to match the document, and `Record<never>` seals the model, so none of those reaches here.");
1540
5154
  }
1541
5155
  return refuse(program, type, `intrinsic "${type.name}"`, "This emitter has no rule for that intrinsic. Replace it with a declared scalar or model.");
5156
+ /**
5157
+ * **openapi3 publishes a tuple as `prefixItems`, with no `items` or `minItems` beside it**:
5158
+ * any length, each position checked where it is present, anything past the last. That is a
5159
+ * tuple whose every position is optional and whose rest is `unknown` - `z.tuple([...],
5160
+ * z.unknown())`, which `z.toJSONSchema` gives back as the same `prefixItems`. Refused as a
5161
+ * parameter - see `parameterPosition`. Refused outright, an OAuth2 flow model, whose `scopes`
5162
+ * is a tuple and which openapi3 publishes as a component, stopped a service compiling.
5163
+ */
5164
+ case "Tuple": {
5165
+ if (parameterPosition)
5166
+ return refuse(program, type, TUPLE_PARAMETER.why, TUPLE_PARAMETER.remedy);
5167
+ const positions = type.values.map((value) => `${typeToZod(program, value)}.exactOptional()`);
5168
+ // `z.tuple` takes at least one position; with none, `prefixItems: []` is any list.
5169
+ return positions.length === 0
5170
+ ? "z.array(z.unknown())"
5171
+ : `z.tuple([${positions.join(", ")}], z.unknown())`;
5172
+ }
1542
5173
  default:
1543
5174
  return refuse(program, type, "no rule for this kind", "Replace it with a model, scalar, enum or union, which are the kinds this emitter walks.");
1544
5175
  }