@kubb/adapter-oas 5.13.1 → 5.14.1

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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineExtension } from "@kubb/core";
2
- import { Diagnostics, ast, childName, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
2
+ import { Diagnostics, ast, childName, enumPrimitive, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
3
3
  import { access, readFile } from "node:fs/promises";
4
4
  import { upgrade } from "@scalar/openapi-upgrader";
5
5
  import { bundle } from "api-ref-bundler";
@@ -20,7 +20,7 @@ var compatibility = {
20
20
  };
21
21
  //#endregion
22
22
  //#region package.json
23
- var version = "5.13.1";
23
+ var version = "5.14.1";
24
24
  //#endregion
25
25
  //#region src/constants.ts
26
26
  /**
@@ -371,6 +371,198 @@ async function read(path) {
371
371
  return readFile(path, { encoding: "utf8" });
372
372
  }
373
373
  //#endregion
374
+ //#region src/oas.ts
375
+ /**
376
+ * Returns `true` when a schema should be treated as nullable.
377
+ *
378
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
379
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
380
+ */
381
+ function isNullable(schema) {
382
+ if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
383
+ const schemaType = schema?.type;
384
+ if (schemaType === "null") return true;
385
+ if (Array.isArray(schemaType)) return schemaType.includes("null");
386
+ return false;
387
+ }
388
+ /**
389
+ * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
390
+ */
391
+ function pickExtensions(source) {
392
+ const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
393
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
394
+ }
395
+ /**
396
+ * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
397
+ */
398
+ function isReference(obj) {
399
+ return !!obj && typeof obj === "object" && "$ref" in obj;
400
+ }
401
+ /**
402
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
403
+ * excluding the Swagger 2 string form.
404
+ */
405
+ function isDiscriminator(obj) {
406
+ const record = obj;
407
+ return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
408
+ }
409
+ /**
410
+ * Returns `true` when a schema is a raw binary payload.
411
+ */
412
+ function isBinary(schema) {
413
+ return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
414
+ }
415
+ /**
416
+ * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
417
+ * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
418
+ * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
419
+ * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
420
+ * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
421
+ * instead of a blob.
422
+ */
423
+ function getBinaryFallbackSchema(mediaType, schema) {
424
+ if (!mediaType || isJsonMimeType(mediaType)) return void 0;
425
+ if (schema && Object.keys(schema).length > 0) return void 0;
426
+ return {
427
+ type: "string",
428
+ contentMediaType: "application/octet-stream"
429
+ };
430
+ }
431
+ /**
432
+ * MIME type fragments that mark a media type as JSON-like.
433
+ *
434
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
435
+ * structured-syntax suffixes such as `application/vnd.api+json`.
436
+ */
437
+ const jsonMimeFragments = [
438
+ "application/json",
439
+ "application/x-json",
440
+ "text/json",
441
+ "text/x-json",
442
+ "+json"
443
+ ];
444
+ /**
445
+ * Returns `true` when a media type string is JSON-like.
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * isJsonMimeType('application/json') // true
450
+ * isJsonMimeType('application/vnd.api+json') // true
451
+ * isJsonMimeType('multipart/form-data') // false
452
+ * ```
453
+ */
454
+ function isJsonMimeType(mimeType) {
455
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
456
+ }
457
+ //#endregion
458
+ //#region src/refs.ts
459
+ /**
460
+ * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
461
+ *
462
+ * @example
463
+ * ```ts
464
+ * const refs = createRefs(document)
465
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet')
466
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
467
+ * refs.resolveNode('#/components/schemas/Pet', parseSchema)
468
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
469
+ * refs.derefKeepingRef(parameter)
470
+ * ```
471
+ */
472
+ function createRefs(document) {
473
+ const pointerCache = /* @__PURE__ */ new Map();
474
+ const resolvedNodeCache = /* @__PURE__ */ new Map();
475
+ const resolvingRefs = /* @__PURE__ */ new Set();
476
+ function walkPointer($ref) {
477
+ const trimmed = $ref.trim();
478
+ if (trimmed === "" || !trimmed.startsWith("#")) return {
479
+ applicable: false,
480
+ value: null
481
+ };
482
+ const pointer = trimmed.substring(1);
483
+ if (pointerCache.has(pointer)) return {
484
+ applicable: true,
485
+ value: pointerCache.get(pointer)
486
+ };
487
+ const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
488
+ if (current) pointerCache.set(pointer, current);
489
+ return {
490
+ applicable: true,
491
+ value: current ?? null
492
+ };
493
+ }
494
+ /**
495
+ * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
496
+ * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
497
+ * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
498
+ * for a speculative lookup where a missing ref is not an error.
499
+ */
500
+ function resolve(refPath, options) {
501
+ const { applicable, value } = walkPointer(refPath);
502
+ if (!applicable || value || options?.report === false) return value;
503
+ const diagnostic = {
504
+ code: Diagnostics.code.refNotFound,
505
+ severity: "error",
506
+ message: `Could not find a definition for ${refPath}.`,
507
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
508
+ location: {
509
+ kind: "schema",
510
+ pointer: refPath,
511
+ ref: refPath
512
+ }
513
+ };
514
+ if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
515
+ return null;
516
+ }
517
+ /**
518
+ * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
519
+ * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
520
+ * resolved (e.g. a minimal document in a unit test).
521
+ */
522
+ function resolveNode(refPath, parse) {
523
+ if (resolvingRefs.has(refPath)) return null;
524
+ if (!resolvedNodeCache.has(refPath)) {
525
+ let resolved = null;
526
+ try {
527
+ const referenced = resolve(refPath);
528
+ if (referenced) {
529
+ resolvingRefs.add(refPath);
530
+ resolved = parse({ schema: referenced });
531
+ resolvingRefs.delete(refPath);
532
+ }
533
+ } catch {}
534
+ resolvedNodeCache.set(refPath, resolved);
535
+ }
536
+ return resolvedNodeCache.get(refPath) ?? null;
537
+ }
538
+ /**
539
+ * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
540
+ * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
541
+ * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
542
+ * returned as-is.
543
+ */
544
+ function deref(value) {
545
+ if (!isReference(value)) return value ? value : null;
546
+ const resolved = resolve(value.$ref);
547
+ return resolved && !isReference(resolved) ? resolved : null;
548
+ }
549
+ /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
550
+ function derefKeepingRef(value) {
551
+ if (isReference(value)) return {
552
+ ...value,
553
+ ...resolve(value.$ref),
554
+ $ref: value.$ref
555
+ };
556
+ return value;
557
+ }
558
+ return {
559
+ resolve,
560
+ resolveNode,
561
+ deref,
562
+ derefKeepingRef
563
+ };
564
+ }
565
+ //#endregion
374
566
  //#region src/load/source.ts
375
567
  const urlRegExp = /^https?:\/+/i;
376
568
  /**
@@ -491,6 +683,22 @@ function hasExternalRef(node) {
491
683
  return Object.values(node).some(hasExternalRef);
492
684
  }
493
685
  /**
686
+ * Replaces each `$ref` into `#/paths/...` with a copy of its target, in place.
687
+ *
688
+ * The Swagger 2.0 upgrade moves `responses/200/schema` under `content`, so a ref to the old location
689
+ * would no longer resolve. A ref that points back into itself is kept, since inlining it never ends.
690
+ */
691
+ function inlinePathRefs(node, resolve, visiting = []) {
692
+ if (!node || typeof node !== "object") return node;
693
+ const ref = node.$ref;
694
+ if (typeof ref === "string" && ref.startsWith("#/paths/") && !visiting.includes(ref)) {
695
+ const target = resolve(ref);
696
+ return target && typeof target === "object" ? inlinePathRefs(structuredClone(target), resolve, [...visiting, ref]) : node;
697
+ }
698
+ for (const [key, value] of Object.entries(node)) node[key] = inlinePathRefs(value, resolve, visiting);
699
+ return node;
700
+ }
701
+ /**
494
702
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
495
703
  *
496
704
  * External file schemas are hoisted into named `components.schemas` entries, so a property
@@ -548,6 +756,17 @@ async function parseFromConfig(source) {
548
756
  await assertInputExists(source.path);
549
757
  return bundleDocument(source.path);
550
758
  })();
759
+ if (raw && typeof raw === "object") {
760
+ const document = raw;
761
+ for (const key of ["swagger", "openapi"]) {
762
+ const version = document[key];
763
+ if (typeof version === "number") document[key] = Number.isInteger(version) ? version.toFixed(1) : String(version);
764
+ }
765
+ }
766
+ if (raw && typeof raw === "object" && "swagger" in raw) {
767
+ const refs = createRefs(raw);
768
+ inlinePathRefs(raw, (ref) => refs.resolve(ref, { report: false }));
769
+ }
551
770
  try {
552
771
  return upgrade(raw, "3.1");
553
772
  } catch (error) {
@@ -644,90 +863,6 @@ async function validateDocumentInWorker(document) {
644
863
  if (!message.valid) reportValidationErrors(message);
645
864
  }
646
865
  //#endregion
647
- //#region src/oas.ts
648
- /**
649
- * Returns `true` when a schema should be treated as nullable.
650
- *
651
- * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
652
- * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
653
- */
654
- function isNullable(schema) {
655
- if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
656
- const schemaType = schema?.type;
657
- if (schemaType === "null") return true;
658
- if (Array.isArray(schemaType)) return schemaType.includes("null");
659
- return false;
660
- }
661
- /**
662
- * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
663
- */
664
- function pickExtensions(source) {
665
- const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
666
- return entries.length > 0 ? Object.fromEntries(entries) : void 0;
667
- }
668
- /**
669
- * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
670
- */
671
- function isReference(obj) {
672
- return !!obj && typeof obj === "object" && "$ref" in obj;
673
- }
674
- /**
675
- * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
676
- * excluding the Swagger 2 string form.
677
- */
678
- function isDiscriminator(obj) {
679
- const record = obj;
680
- return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
681
- }
682
- /**
683
- * Returns `true` when a schema is a raw binary payload.
684
- */
685
- function isBinary(schema) {
686
- return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
687
- }
688
- /**
689
- * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
690
- * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
691
- * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
692
- * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
693
- * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
694
- * instead of a blob.
695
- */
696
- function getBinaryFallbackSchema(mediaType, schema) {
697
- if (!mediaType || isJsonMimeType(mediaType)) return void 0;
698
- if (schema && Object.keys(schema).length > 0) return void 0;
699
- return {
700
- type: "string",
701
- contentMediaType: "application/octet-stream"
702
- };
703
- }
704
- /**
705
- * MIME type fragments that mark a media type as JSON-like.
706
- *
707
- * A content type is JSON when it contains any of these substrings. The `+json` entry catches
708
- * structured-syntax suffixes such as `application/vnd.api+json`.
709
- */
710
- const jsonMimeFragments = [
711
- "application/json",
712
- "application/x-json",
713
- "text/json",
714
- "text/x-json",
715
- "+json"
716
- ];
717
- /**
718
- * Returns `true` when a media type string is JSON-like.
719
- *
720
- * @example
721
- * ```ts
722
- * isJsonMimeType('application/json') // true
723
- * isJsonMimeType('application/vnd.api+json') // true
724
- * isJsonMimeType('multipart/form-data') // false
725
- * ```
726
- */
727
- function isJsonMimeType(mimeType) {
728
- return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
729
- }
730
- //#endregion
731
866
  //#region src/model/components.ts
732
867
  /**
733
868
  * Extracts the inline schema from a media-type `content` map, with the media type it sits under.
@@ -1156,6 +1291,19 @@ function getParameters({ operation, refs }) {
1156
1291
  for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
1157
1292
  return Array.from(paramMap.values());
1158
1293
  }
1294
+ /** The `encoding` of a media type, reduced to the entries that set a field Kubb carries. */
1295
+ function getEncoding(encoding) {
1296
+ const entries = Object.entries(encoding ?? {}).flatMap(([property, { contentType, style, explode, allowReserved }]) => {
1297
+ const node = {
1298
+ ...contentType !== void 0 ? { contentType } : {},
1299
+ ...style !== void 0 ? { style } : {},
1300
+ ...explode !== void 0 ? { explode } : {},
1301
+ ...allowReserved !== void 0 ? { allowReserved } : {}
1302
+ };
1303
+ return Object.keys(node).length ? [[property, node]] : [];
1304
+ });
1305
+ return entries.length ? Object.fromEntries(entries) : void 0;
1306
+ }
1159
1307
  /** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
1160
1308
  function getBodySchema({ content, contentType, refs }) {
1161
1309
  const entry = contentType ? content?.[contentType] : void 0;
@@ -1263,10 +1411,12 @@ function parseOperation({ operation, ...ctx }) {
1263
1411
  name: requestBodyName
1264
1412
  });
1265
1413
  scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
1414
+ const encoding = getEncoding(body?.content?.[ct]?.encoding);
1266
1415
  return [ast.factory.createContent({
1267
1416
  contentType: ct,
1268
1417
  schema: ast.optionality(node, bodyRequired),
1269
- keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1418
+ keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
1419
+ ...encoding && { encoding }
1270
1420
  })];
1271
1421
  });
1272
1422
  const requestBody = content.length > 0 || body?.description ? {
@@ -1363,6 +1513,42 @@ function createNode({ schema, name, nullable, defaultValue }, extras) {
1363
1513
  //#endregion
1364
1514
  //#region src/emit/discriminator/preserve.ts
1365
1515
  /**
1516
+ * Turns string mapping keys into the discriminator property's type when they parse cleanly.
1517
+ */
1518
+ function typedKeys({ keys, propertyType }) {
1519
+ return keys.map((key) => {
1520
+ if (propertyType === "integer" || propertyType === "number") {
1521
+ const value = Number(key);
1522
+ return key.trim() !== "" && Number.isFinite(value) ? value : key;
1523
+ }
1524
+ if (propertyType === "boolean") {
1525
+ const lower = key.toLowerCase();
1526
+ return lower === "true" ? true : lower === "false" ? false : key;
1527
+ }
1528
+ return key;
1529
+ });
1530
+ }
1531
+ /**
1532
+ * Reads the declared `type` of `propertyName` on a schema, following `$ref`s and `allOf` members.
1533
+ * A nullable array such as `['integer', 'null']` reads as its one non-null type.
1534
+ */
1535
+ function propertyTypeOf({ schema, propertyName, refs, seen = /* @__PURE__ */ new Set() }) {
1536
+ const deref = (value) => value && isReference(value) ? refs.resolve(value.$ref, { report: false }) : value;
1537
+ if (schema && isReference(schema)) {
1538
+ if (seen.has(schema.$ref)) return void 0;
1539
+ seen.add(schema.$ref);
1540
+ }
1541
+ const resolved = deref(schema);
1542
+ const type = deref(resolved?.properties?.[propertyName])?.type ?? resolved?.allOf?.map((member) => propertyTypeOf({
1543
+ schema: member,
1544
+ propertyName,
1545
+ refs,
1546
+ seen
1547
+ })).find(Boolean);
1548
+ const nonNull = Array.isArray(type) ? type.filter((t) => t !== "null") : [];
1549
+ return nonNull.length === 1 ? nonNull[0] : type;
1550
+ }
1551
+ /**
1366
1552
  * Creates a single-property object schema used as a discriminator literal.
1367
1553
  *
1368
1554
  * @example
@@ -1379,7 +1565,7 @@ function createDiscriminantNode({ propertyName, values }) {
1379
1565
  name: propertyName,
1380
1566
  schema: ast.factory.createSchema({
1381
1567
  type: "enum",
1382
- primitive: "string",
1568
+ primitive: enumPrimitive(values),
1383
1569
  enumValues: values
1384
1570
  }),
1385
1571
  required: true
@@ -1394,21 +1580,61 @@ function createDiscriminantNode({ propertyName, values }) {
1394
1580
  *
1395
1581
  * @example
1396
1582
  * ```ts
1397
- * findDiscriminators({ dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // ['dog', 'hound']
1583
+ * findDiscriminators({ mapping: { dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, ref: '#/components/schemas/Dog' }) // ['dog', 'hound']
1398
1584
  * ```
1399
1585
  */
1400
- function findDiscriminators(mapping, ref) {
1586
+ function findDiscriminators({ mapping, ref }) {
1401
1587
  if (!mapping || !ref) return [];
1402
1588
  return Object.entries(mapping).filter(([, value]) => value === ref).map(([key]) => key);
1403
1589
  }
1404
1590
  /**
1591
+ * Finds the discriminator a schema declares itself or through an inline `allOf` member.
1592
+ */
1593
+ function getDiscriminator(schema) {
1594
+ return (isDiscriminator(schema) ? schema : schema.allOf?.find(isDiscriminator))?.discriminator;
1595
+ }
1596
+ /**
1597
+ * Lists the mapping keys of a discriminator plus the keys of the nested discriminators its mapped
1598
+ * schemas declare on the same property, so a root `$type` accepts values from every level.
1599
+ */
1600
+ function getMappingKeys({ discriminator: { propertyName, mapping }, refs, seen = /* @__PURE__ */ new Set() }) {
1601
+ return Object.entries(mapping ?? {}).flatMap(([key, target]) => {
1602
+ if (seen.has(target)) return [key];
1603
+ seen.add(target);
1604
+ const resolved = refs.resolve(target, { report: false });
1605
+ const nested = resolved && getDiscriminator(resolved);
1606
+ return nested?.propertyName === propertyName ? [key, ...getMappingKeys({
1607
+ discriminator: nested,
1608
+ refs,
1609
+ seen
1610
+ })] : [key];
1611
+ });
1612
+ }
1613
+ /**
1614
+ * Drops the discriminator from an inline `allOf` member when a `$ref` sibling already declares one on
1615
+ * the same property. That mapping names the schema's own children, and keeping it pins the property
1616
+ * to those keys next to the parent's, which leaves `never`.
1617
+ */
1618
+ function removeInheritedDiscriminator({ members, refs }) {
1619
+ const inherited = new Set(members.flatMap((member) => {
1620
+ const parent = isReference(member) ? refs.resolve(member.$ref, { report: false }) : null;
1621
+ return parent && getDiscriminator(parent)?.propertyName || [];
1622
+ }));
1623
+ return members.map((member) => {
1624
+ if (isReference(member) || !isDiscriminator(member) || member.oneOf || member.anyOf) return member;
1625
+ if (!inherited.has(member.discriminator.propertyName)) return member;
1626
+ const { discriminator: _discriminator, ...rest } = member;
1627
+ return rest;
1628
+ });
1629
+ }
1630
+ /**
1405
1631
  * Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
1406
1632
  * node with either the shared-properties slice carrying that value, or a synthetic discriminant
1407
1633
  * literal. The referenced child schema's own definition is left untouched — narrowing happens only
1408
1634
  * at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
1409
1635
  * which additionally patches the child schema's own definition in a post-pass).
1410
1636
  */
1411
- function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, name, refs }) {
1637
+ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parentType, parse, name, refs }) {
1412
1638
  function pickDiscriminatorPropertyNode(node, propertyName) {
1413
1639
  const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1414
1640
  if (!discriminatorProperty) return null;
@@ -1444,9 +1670,20 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
1444
1670
  }
1445
1671
  return unionMembers.map((s) => {
1446
1672
  const ref = isReference(s) ? s.$ref : void 0;
1447
- const mappedValues = findDiscriminators(discriminator?.mapping, ref);
1673
+ const mappedValues = findDiscriminators({
1674
+ mapping: discriminator?.mapping,
1675
+ ref
1676
+ });
1448
1677
  const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
1449
- const discriminatorValues = mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [];
1678
+ const propertyType = propertyTypeOf({
1679
+ schema: s,
1680
+ propertyName: discriminator?.propertyName ?? "",
1681
+ refs
1682
+ }) ?? parentType;
1683
+ const discriminatorValues = typedKeys({
1684
+ keys: mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [],
1685
+ propertyType
1686
+ });
1450
1687
  const memberNode = parse({
1451
1688
  schema: s,
1452
1689
  name
@@ -1478,21 +1715,34 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1478
1715
  members: allOfMembers.filter((item) => {
1479
1716
  if (!isReference(item) || !name) return true;
1480
1717
  const deref = refs.resolve(item.$ref);
1481
- if (!deref || !isDiscriminator(deref)) return true;
1482
- const parentUnion = deref.oneOf ?? deref.anyOf;
1483
- if (!parentUnion) return true;
1718
+ const discriminator = deref && getDiscriminator(deref);
1719
+ const parentUnion = deref?.oneOf ?? deref?.anyOf;
1720
+ if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
1484
1721
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1485
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1486
- const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1487
- if (inOneOf || inMapping) {
1488
- const values = findDiscriminators(deref.discriminator.mapping, childRef);
1489
- if (values.length) discriminantValues.push({
1490
- propertyName: deref.discriminator.propertyName,
1491
- values
1722
+ const values = findDiscriminators({
1723
+ mapping: discriminator.mapping,
1724
+ ref: childRef
1725
+ });
1726
+ if (values.length) {
1727
+ const { propertyName } = discriminator;
1728
+ const propertyType = propertyTypeOf({
1729
+ schema: deref,
1730
+ propertyName,
1731
+ refs
1732
+ }) ?? propertyTypeOf({
1733
+ schema: { $ref: childRef },
1734
+ propertyName,
1735
+ refs
1736
+ });
1737
+ discriminantValues.push({
1738
+ propertyName,
1739
+ values: typedKeys({
1740
+ keys: values,
1741
+ propertyType
1742
+ })
1492
1743
  });
1493
- return false;
1494
1744
  }
1495
- return true;
1745
+ return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
1496
1746
  }),
1497
1747
  discriminantValues
1498
1748
  };
@@ -1619,10 +1869,9 @@ function resolveUnionMembers(members, siblingProperties) {
1619
1869
  });
1620
1870
  }
1621
1871
  /**
1622
- * Enriches any `oneOf` or `anyOf` branches on a schema object with properties from sibling schemas.
1872
+ * Enriches a schema object, and any `oneOf` or `anyOf` branches on it, with properties from sibling schemas.
1623
1873
  */
1624
1874
  function resolveUnionSchema(schema, siblingProperties) {
1625
- if (!schema.oneOf && !schema.anyOf) return schema;
1626
1875
  const next = { ...resolveUnionMembers([schema], siblingProperties)[0] ?? schema };
1627
1876
  if (schema.oneOf) next.oneOf = resolveUnionMembers(schema.oneOf, siblingProperties);
1628
1877
  if (schema.anyOf) next.anyOf = resolveUnionMembers(schema.anyOf, siblingProperties);
@@ -1646,6 +1895,17 @@ function convertRef({ schema, name, nullable, defaultValue, document, parse, ref
1646
1895
  defaultValue
1647
1896
  };
1648
1897
  if (refPath && document.components && !refs.resolve(refPath, { report: false })) return createNode(ctx, { type: "unknown" });
1898
+ if (refPath && !refPath.startsWith("#/components/schemas/") && resolvedSchema) return ast.factory.createSchema({
1899
+ ...resolvedSchema,
1900
+ title: schema.title ?? resolvedSchema.title,
1901
+ description: schema.description ?? resolvedSchema.description,
1902
+ deprecated: schema.deprecated ?? resolvedSchema.deprecated,
1903
+ readOnly: schema.readOnly ?? resolvedSchema.readOnly,
1904
+ writeOnly: schema.writeOnly ?? resolvedSchema.writeOnly,
1905
+ default: defaultValue ?? resolvedSchema.default,
1906
+ name: name ?? resolvedSchema.name,
1907
+ nullable: nullable ?? resolvedSchema.nullable
1908
+ });
1649
1909
  const targetName = renames?.get(schema.$ref);
1650
1910
  return createNode(ctx, {
1651
1911
  type: "ref",
@@ -1677,9 +1937,41 @@ function convertNot({ schema, name, nullable, defaultValue, parse }) {
1677
1937
  });
1678
1938
  }
1679
1939
  /**
1940
+ * Keywords that describe a schema or conditionally constrain it, but add no structure.
1941
+ */
1942
+ const annotationKeys = /* @__PURE__ */ new Set([
1943
+ "description",
1944
+ "title",
1945
+ "example",
1946
+ "examples",
1947
+ "default",
1948
+ "deprecated",
1949
+ "readOnly",
1950
+ "writeOnly",
1951
+ "if",
1952
+ "then",
1953
+ "else"
1954
+ ]);
1955
+ /**
1956
+ * Moves `allOf` members that hold only annotations or `if`/`then`/`else` onto the parent, so they do
1957
+ * not become `unknown`. The parent's own values win, and the conditional keywords are not enforced.
1958
+ */
1959
+ function mergeAnnotations(schema) {
1960
+ const members = schema.allOf;
1961
+ const isAnnotation = (member) => !isReference(member) && Object.keys(member).every((key) => annotationKeys.has(key));
1962
+ const annotations = members.filter(isAnnotation);
1963
+ const rest = members.filter((member) => !isAnnotation(member));
1964
+ return annotations.length && rest.length ? {
1965
+ ...Object.assign({}, ...annotations),
1966
+ ...schema,
1967
+ allOf: rest
1968
+ } : schema;
1969
+ }
1970
+ /**
1680
1971
  * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1681
1972
  */
1682
- function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1973
+ function convertAllOf({ schema: original, name, nullable, defaultValue, parse, refs }) {
1974
+ const schema = mergeAnnotations(original);
1683
1975
  if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1684
1976
  const [memberSchema] = schema.allOf;
1685
1977
  const memberNode = parse({
@@ -1705,7 +1997,10 @@ function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1705
1997
  });
1706
1998
  }
1707
1999
  const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
1708
- allOfMembers: schema.allOf,
2000
+ allOfMembers: removeInheritedDiscriminator({
2001
+ members: schema.allOf,
2002
+ refs
2003
+ }),
1709
2004
  name,
1710
2005
  refs
1711
2006
  });
@@ -1785,6 +2080,11 @@ function convertUnion(context) {
1785
2080
  unionMembers,
1786
2081
  discriminator,
1787
2082
  sharedPropertiesNode,
2083
+ parentType: discriminator && propertyTypeOf({
2084
+ schema,
2085
+ propertyName: discriminator.propertyName,
2086
+ refs
2087
+ }),
1788
2088
  parse,
1789
2089
  name,
1790
2090
  refs
@@ -2048,7 +2348,7 @@ function nameEnums(node, options) {
2048
2348
  /**
2049
2349
  * Converts an object-like schema into an `ObjectSchemaNode`.
2050
2350
  */
2051
- function convertObject({ schema, name, nullable, defaultValue, options, parse }) {
2351
+ function convertObject({ schema, name, nullable, defaultValue, options, parse, refs }) {
2052
2352
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
2053
2353
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
2054
2354
  const resolvedPropSchema = propSchema;
@@ -2065,7 +2365,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2065
2365
  name: propName,
2066
2366
  schema: {
2067
2367
  ...schemaNode,
2068
- nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
2368
+ nullable: schemaNode.type === "null" ? void 0 : propNullable || schemaNode.nullable || void 0
2069
2369
  },
2070
2370
  required
2071
2371
  });
@@ -2105,7 +2405,17 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2105
2405
  });
2106
2406
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
2107
2407
  const discPropName = schema.discriminator.propertyName;
2108
- const values = Object.keys(schema.discriminator.mapping);
2408
+ const values = typedKeys({
2409
+ keys: getMappingKeys({
2410
+ discriminator: schema.discriminator,
2411
+ refs
2412
+ }),
2413
+ propertyType: propertyTypeOf({
2414
+ schema,
2415
+ propertyName: discPropName,
2416
+ refs
2417
+ })
2418
+ });
2109
2419
  const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
2110
2420
  return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
2111
2421
  propertyName: discPropName,
@@ -2163,8 +2473,9 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
2163
2473
  //#region src/emit/parseSchema.ts
2164
2474
  /**
2165
2475
  * Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
2166
- * `oneOf`/`anyOf`) take precedence over `const`, which takes precedence over a genuine OAS 3.1
2167
- * multi-type array (more than one non-`null` type), which takes precedence over `format`/`type`.
2476
+ * `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
2477
+ * `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
2478
+ * non-`null` type), which takes precedence over `format`/`type`.
2168
2479
  * A multi-type array must split into its per-type members before `format` runs, otherwise
2169
2480
  * `format` collapses the whole schema to one type (e.g. `type: ['null', 'integer', 'string'],
2170
2481
  * format: 'int32'` would drop `string` and emit just `integer`); each split-off member still
@@ -2178,7 +2489,7 @@ const schemaRules = [
2178
2489
  convert: convertNot
2179
2490
  },
2180
2491
  {
2181
- match: ({ schema }) => isReference(schema),
2492
+ match: ({ schema }) => isReference(schema) && !("const" in schema && schema.const !== void 0),
2182
2493
  convert: convertRef
2183
2494
  },
2184
2495
  {
@@ -2308,7 +2619,7 @@ function createSchemaParser(ctx) {
2308
2619
  function collectInlineEnums(roots, topLevelNames) {
2309
2620
  const promoted = /* @__PURE__ */ new Map();
2310
2621
  for (const root of roots) {
2311
- const isSchemaRoot = root.kind === "Schema";
2622
+ const isSchemaRoot = root.kind === ast.nodeKinds.schema;
2312
2623
  for (const node of ast.collect(root, { schema: (schemaNode) => schemaNode })) {
2313
2624
  if (node.type !== "enum" || !node.name) continue;
2314
2625
  if (isSchemaRoot && node === root) continue;
@@ -2346,114 +2657,6 @@ function refPromotedEnums(node, promoted) {
2346
2657
  } });
2347
2658
  }
2348
2659
  //#endregion
2349
- //#region src/refs.ts
2350
- /**
2351
- * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
2352
- *
2353
- * @example
2354
- * ```ts
2355
- * const refs = createRefs(document)
2356
- * refs.resolve<SchemaObject>('#/components/schemas/Pet')
2357
- * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
2358
- * refs.resolveNode('#/components/schemas/Pet', parseSchema)
2359
- * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
2360
- * refs.derefKeepingRef(parameter)
2361
- * ```
2362
- */
2363
- function createRefs(document) {
2364
- const pointerCache = /* @__PURE__ */ new Map();
2365
- const resolvedNodeCache = /* @__PURE__ */ new Map();
2366
- const resolvingRefs = /* @__PURE__ */ new Set();
2367
- function walkPointer($ref) {
2368
- const trimmed = $ref.trim();
2369
- if (trimmed === "" || !trimmed.startsWith("#")) return {
2370
- applicable: false,
2371
- value: null
2372
- };
2373
- const pointer = trimmed.substring(1);
2374
- if (pointerCache.has(pointer)) return {
2375
- applicable: true,
2376
- value: pointerCache.get(pointer)
2377
- };
2378
- const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
2379
- if (current) pointerCache.set(pointer, current);
2380
- return {
2381
- applicable: true,
2382
- value: current ?? null
2383
- };
2384
- }
2385
- /**
2386
- * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
2387
- * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
2388
- * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
2389
- * for a speculative lookup where a missing ref is not an error.
2390
- */
2391
- function resolve(refPath, options) {
2392
- const { applicable, value } = walkPointer(refPath);
2393
- if (!applicable || value || options?.report === false) return value;
2394
- const diagnostic = {
2395
- code: Diagnostics.code.refNotFound,
2396
- severity: "error",
2397
- message: `Could not find a definition for ${refPath}.`,
2398
- help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
2399
- location: {
2400
- kind: "schema",
2401
- pointer: refPath,
2402
- ref: refPath
2403
- }
2404
- };
2405
- if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
2406
- return null;
2407
- }
2408
- /**
2409
- * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
2410
- * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
2411
- * resolved (e.g. a minimal document in a unit test).
2412
- */
2413
- function resolveNode(refPath, parse) {
2414
- if (resolvingRefs.has(refPath)) return null;
2415
- if (!resolvedNodeCache.has(refPath)) {
2416
- let resolved = null;
2417
- try {
2418
- const referenced = resolve(refPath);
2419
- if (referenced) {
2420
- resolvingRefs.add(refPath);
2421
- resolved = parse({ schema: referenced });
2422
- resolvingRefs.delete(refPath);
2423
- }
2424
- } catch {}
2425
- resolvedNodeCache.set(refPath, resolved);
2426
- }
2427
- return resolvedNodeCache.get(refPath) ?? null;
2428
- }
2429
- /**
2430
- * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
2431
- * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
2432
- * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
2433
- * returned as-is.
2434
- */
2435
- function deref(value) {
2436
- if (!isReference(value)) return value ? value : null;
2437
- const resolved = resolve(value.$ref);
2438
- return resolved && !isReference(resolved) ? resolved : null;
2439
- }
2440
- /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
2441
- function derefKeepingRef(value) {
2442
- if (isReference(value)) return {
2443
- ...value,
2444
- ...resolve(value.$ref),
2445
- $ref: value.$ref
2446
- };
2447
- return value;
2448
- }
2449
- return {
2450
- resolve,
2451
- resolveNode,
2452
- deref,
2453
- derefKeepingRef
2454
- };
2455
- }
2456
- //#endregion
2457
2660
  //#region src/sourceLocator.ts
2458
2661
  let cached;
2459
2662
  const toRange = ([start, end]) => ({
@@ -2687,7 +2890,7 @@ const adapterOas = defineExtension({
2687
2890
  };
2688
2891
  parsedByName.set(name, node);
2689
2892
  scanSchema(node);
2690
- if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2893
+ if (node.type === "ref" && node.name) refAliasMap.set(name, node);
2691
2894
  if (ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
2692
2895
  if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2693
2896
  }
@@ -2702,6 +2905,32 @@ const adapterOas = defineExtension({
2702
2905
  }));
2703
2906
  const promotedEnums = enums === "root" ? collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas))) : null;
2704
2907
  if (promotedEnums) for (const name of promotedEnums.keys()) enumNames.push(name);
2908
+ function isAliasCycle(name) {
2909
+ const seen = /* @__PURE__ */ new Set([name]);
2910
+ for (let next = refAliasMap.get(name)?.name; next; next = refAliasMap.get(next)?.name) {
2911
+ if (next === name) return true;
2912
+ if (seen.has(next)) return false;
2913
+ seen.add(next);
2914
+ }
2915
+ return false;
2916
+ }
2917
+ function createCycleNode(name, pointer) {
2918
+ Diagnostics.report({
2919
+ code: Diagnostics.code.aliasCycle,
2920
+ severity: "warning",
2921
+ message: `${name} only refers to other schemas that lead back to it, so it is resolved to unknown.`,
2922
+ help: "Give one of the schemas in the loop a real definition, or remove the `$ref` that closes it.",
2923
+ location: {
2924
+ kind: "schema",
2925
+ pointer: pointer ?? `#/components/schemas/${name}`
2926
+ }
2927
+ });
2928
+ return ast.factory.createSchema({
2929
+ type: "unknown",
2930
+ name,
2931
+ pointer
2932
+ });
2933
+ }
2705
2934
  const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
2706
2935
  for (const name of Object.keys(schemas)) {
2707
2936
  const aliasTarget = refAliasMap.get(name)?.name;
@@ -2709,11 +2938,12 @@ const adapterOas = defineExtension({
2709
2938
  const parsed = parsedByName.get(name);
2710
2939
  const child = discriminatorChildMap?.get(name);
2711
2940
  const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
2712
- const node = aliased ? {
2941
+ const resolved = aliased ? {
2713
2942
  ...aliased,
2714
2943
  name,
2715
2944
  pointer: parsed.pointer
2716
2945
  } : base;
2946
+ const node = isAliasCycle(name) ? createCycleNode(name, parsed.pointer) : resolved;
2717
2947
  schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
2718
2948
  }
2719
2949
  const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;