@kubb/adapter-oas 5.14.0 → 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.cjs CHANGED
@@ -36,7 +36,7 @@ var compatibility = {
36
36
  };
37
37
  //#endregion
38
38
  //#region package.json
39
- var version = "5.14.0";
39
+ var version = "5.14.1";
40
40
  //#endregion
41
41
  //#region src/constants.ts
42
42
  /**
@@ -387,6 +387,198 @@ async function read(path) {
387
387
  return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
388
388
  }
389
389
  //#endregion
390
+ //#region src/oas.ts
391
+ /**
392
+ * Returns `true` when a schema should be treated as nullable.
393
+ *
394
+ * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
395
+ * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
396
+ */
397
+ function isNullable(schema) {
398
+ if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
399
+ const schemaType = schema?.type;
400
+ if (schemaType === "null") return true;
401
+ if (Array.isArray(schemaType)) return schemaType.includes("null");
402
+ return false;
403
+ }
404
+ /**
405
+ * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
406
+ */
407
+ function pickExtensions(source) {
408
+ const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
409
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
410
+ }
411
+ /**
412
+ * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
413
+ */
414
+ function isReference(obj) {
415
+ return !!obj && typeof obj === "object" && "$ref" in obj;
416
+ }
417
+ /**
418
+ * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
419
+ * excluding the Swagger 2 string form.
420
+ */
421
+ function isDiscriminator(obj) {
422
+ const record = obj;
423
+ return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
424
+ }
425
+ /**
426
+ * Returns `true` when a schema is a raw binary payload.
427
+ */
428
+ function isBinary(schema) {
429
+ return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
430
+ }
431
+ /**
432
+ * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
433
+ * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
434
+ * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
435
+ * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
436
+ * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
437
+ * instead of a blob.
438
+ */
439
+ function getBinaryFallbackSchema(mediaType, schema) {
440
+ if (!mediaType || isJsonMimeType(mediaType)) return void 0;
441
+ if (schema && Object.keys(schema).length > 0) return void 0;
442
+ return {
443
+ type: "string",
444
+ contentMediaType: "application/octet-stream"
445
+ };
446
+ }
447
+ /**
448
+ * MIME type fragments that mark a media type as JSON-like.
449
+ *
450
+ * A content type is JSON when it contains any of these substrings. The `+json` entry catches
451
+ * structured-syntax suffixes such as `application/vnd.api+json`.
452
+ */
453
+ const jsonMimeFragments = [
454
+ "application/json",
455
+ "application/x-json",
456
+ "text/json",
457
+ "text/x-json",
458
+ "+json"
459
+ ];
460
+ /**
461
+ * Returns `true` when a media type string is JSON-like.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * isJsonMimeType('application/json') // true
466
+ * isJsonMimeType('application/vnd.api+json') // true
467
+ * isJsonMimeType('multipart/form-data') // false
468
+ * ```
469
+ */
470
+ function isJsonMimeType(mimeType) {
471
+ return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
472
+ }
473
+ //#endregion
474
+ //#region src/refs.ts
475
+ /**
476
+ * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * const refs = createRefs(document)
481
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet')
482
+ * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
483
+ * refs.resolveNode('#/components/schemas/Pet', parseSchema)
484
+ * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
485
+ * refs.derefKeepingRef(parameter)
486
+ * ```
487
+ */
488
+ function createRefs(document) {
489
+ const pointerCache = /* @__PURE__ */ new Map();
490
+ const resolvedNodeCache = /* @__PURE__ */ new Map();
491
+ const resolvingRefs = /* @__PURE__ */ new Set();
492
+ function walkPointer($ref) {
493
+ const trimmed = $ref.trim();
494
+ if (trimmed === "" || !trimmed.startsWith("#")) return {
495
+ applicable: false,
496
+ value: null
497
+ };
498
+ const pointer = trimmed.substring(1);
499
+ if (pointerCache.has(pointer)) return {
500
+ applicable: true,
501
+ value: pointerCache.get(pointer)
502
+ };
503
+ const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
504
+ if (current) pointerCache.set(pointer, current);
505
+ return {
506
+ applicable: true,
507
+ value: current ?? null
508
+ };
509
+ }
510
+ /**
511
+ * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
512
+ * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
513
+ * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
514
+ * for a speculative lookup where a missing ref is not an error.
515
+ */
516
+ function resolve(refPath, options) {
517
+ const { applicable, value } = walkPointer(refPath);
518
+ if (!applicable || value || options?.report === false) return value;
519
+ const diagnostic = {
520
+ code: _kubb_kit.Diagnostics.code.refNotFound,
521
+ severity: "error",
522
+ message: `Could not find a definition for ${refPath}.`,
523
+ help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
524
+ location: {
525
+ kind: "schema",
526
+ pointer: refPath,
527
+ ref: refPath
528
+ }
529
+ };
530
+ if (!_kubb_kit.Diagnostics.report(diagnostic)) throw new _kubb_kit.Diagnostics.Error(diagnostic);
531
+ return null;
532
+ }
533
+ /**
534
+ * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
535
+ * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
536
+ * resolved (e.g. a minimal document in a unit test).
537
+ */
538
+ function resolveNode(refPath, parse) {
539
+ if (resolvingRefs.has(refPath)) return null;
540
+ if (!resolvedNodeCache.has(refPath)) {
541
+ let resolved = null;
542
+ try {
543
+ const referenced = resolve(refPath);
544
+ if (referenced) {
545
+ resolvingRefs.add(refPath);
546
+ resolved = parse({ schema: referenced });
547
+ resolvingRefs.delete(refPath);
548
+ }
549
+ } catch {}
550
+ resolvedNodeCache.set(refPath, resolved);
551
+ }
552
+ return resolvedNodeCache.get(refPath) ?? null;
553
+ }
554
+ /**
555
+ * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
556
+ * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
557
+ * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
558
+ * returned as-is.
559
+ */
560
+ function deref(value) {
561
+ if (!isReference(value)) return value ? value : null;
562
+ const resolved = resolve(value.$ref);
563
+ return resolved && !isReference(resolved) ? resolved : null;
564
+ }
565
+ /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
566
+ function derefKeepingRef(value) {
567
+ if (isReference(value)) return {
568
+ ...value,
569
+ ...resolve(value.$ref),
570
+ $ref: value.$ref
571
+ };
572
+ return value;
573
+ }
574
+ return {
575
+ resolve,
576
+ resolveNode,
577
+ deref,
578
+ derefKeepingRef
579
+ };
580
+ }
581
+ //#endregion
390
582
  //#region src/load/source.ts
391
583
  const urlRegExp = /^https?:\/+/i;
392
584
  /**
@@ -507,6 +699,22 @@ function hasExternalRef(node) {
507
699
  return Object.values(node).some(hasExternalRef);
508
700
  }
509
701
  /**
702
+ * Replaces each `$ref` into `#/paths/...` with a copy of its target, in place.
703
+ *
704
+ * The Swagger 2.0 upgrade moves `responses/200/schema` under `content`, so a ref to the old location
705
+ * would no longer resolve. A ref that points back into itself is kept, since inlining it never ends.
706
+ */
707
+ function inlinePathRefs(node, resolve, visiting = []) {
708
+ if (!node || typeof node !== "object") return node;
709
+ const ref = node.$ref;
710
+ if (typeof ref === "string" && ref.startsWith("#/paths/") && !visiting.includes(ref)) {
711
+ const target = resolve(ref);
712
+ return target && typeof target === "object" ? inlinePathRefs(structuredClone(target), resolve, [...visiting, ref]) : node;
713
+ }
714
+ for (const [key, value] of Object.entries(node)) node[key] = inlinePathRefs(value, resolve, visiting);
715
+ return node;
716
+ }
717
+ /**
510
718
  * Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
511
719
  *
512
720
  * External file schemas are hoisted into named `components.schemas` entries, so a property
@@ -564,6 +772,17 @@ async function parseFromConfig(source) {
564
772
  await assertInputExists(source.path);
565
773
  return bundleDocument(source.path);
566
774
  })();
775
+ if (raw && typeof raw === "object") {
776
+ const document = raw;
777
+ for (const key of ["swagger", "openapi"]) {
778
+ const version = document[key];
779
+ if (typeof version === "number") document[key] = Number.isInteger(version) ? version.toFixed(1) : String(version);
780
+ }
781
+ }
782
+ if (raw && typeof raw === "object" && "swagger" in raw) {
783
+ const refs = createRefs(raw);
784
+ inlinePathRefs(raw, (ref) => refs.resolve(ref, { report: false }));
785
+ }
567
786
  try {
568
787
  return (0, _scalar_openapi_upgrader.upgrade)(raw, "3.1");
569
788
  } catch (error) {
@@ -660,90 +879,6 @@ async function validateDocumentInWorker(document) {
660
879
  if (!message.valid) reportValidationErrors(message);
661
880
  }
662
881
  //#endregion
663
- //#region src/oas.ts
664
- /**
665
- * Returns `true` when a schema should be treated as nullable.
666
- *
667
- * Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
668
- * `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
669
- */
670
- function isNullable(schema) {
671
- if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
672
- const schemaType = schema?.type;
673
- if (schemaType === "null") return true;
674
- if (Array.isArray(schemaType)) return schemaType.includes("null");
675
- return false;
676
- }
677
- /**
678
- * Returns the `x-*` keys of `source` as-is, or `undefined` when there are none.
679
- */
680
- function pickExtensions(source) {
681
- const entries = Object.entries(source).filter(([key]) => key.startsWith("x-"));
682
- return entries.length > 0 ? Object.fromEntries(entries) : void 0;
683
- }
684
- /**
685
- * Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
686
- */
687
- function isReference(obj) {
688
- return !!obj && typeof obj === "object" && "$ref" in obj;
689
- }
690
- /**
691
- * Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
692
- * excluding the Swagger 2 string form.
693
- */
694
- function isDiscriminator(obj) {
695
- const record = obj;
696
- return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
697
- }
698
- /**
699
- * Returns `true` when a schema is a raw binary payload.
700
- */
701
- function isBinary(schema) {
702
- return schema.contentMediaType === "application/octet-stream" && schema.contentEncoding === void 0 && (schema.type === void 0 || schema.type === "string");
703
- }
704
- /**
705
- * Returns the binary schema for a non-JSON entry the OAS 3.1 upgrade emptied out, or `undefined`
706
- * when the entry carries a schema of its own. The upgrade rewrites `format: 'binary'` into
707
- * `contentMediaType` and drops it whenever it only repeats the media type key, for any binary
708
- * media type (`application/octet-stream`, `application/pdf`, `image/png`, ...), not just
709
- * `application/octet-stream`. Without the fallback such a body resolves to the empty schema type
710
- * instead of a blob.
711
- */
712
- function getBinaryFallbackSchema(mediaType, schema) {
713
- if (!mediaType || isJsonMimeType(mediaType)) return void 0;
714
- if (schema && Object.keys(schema).length > 0) return void 0;
715
- return {
716
- type: "string",
717
- contentMediaType: "application/octet-stream"
718
- };
719
- }
720
- /**
721
- * MIME type fragments that mark a media type as JSON-like.
722
- *
723
- * A content type is JSON when it contains any of these substrings. The `+json` entry catches
724
- * structured-syntax suffixes such as `application/vnd.api+json`.
725
- */
726
- const jsonMimeFragments = [
727
- "application/json",
728
- "application/x-json",
729
- "text/json",
730
- "text/x-json",
731
- "+json"
732
- ];
733
- /**
734
- * Returns `true` when a media type string is JSON-like.
735
- *
736
- * @example
737
- * ```ts
738
- * isJsonMimeType('application/json') // true
739
- * isJsonMimeType('application/vnd.api+json') // true
740
- * isJsonMimeType('multipart/form-data') // false
741
- * ```
742
- */
743
- function isJsonMimeType(mimeType) {
744
- return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
745
- }
746
- //#endregion
747
882
  //#region src/model/components.ts
748
883
  /**
749
884
  * Extracts the inline schema from a media-type `content` map, with the media type it sits under.
@@ -1172,6 +1307,19 @@ function getParameters({ operation, refs }) {
1172
1307
  for (const entry of [...pathLevelParams, ...operationParams]) if (entry.param.name && entry.param.in) paramMap.set(`${entry.param.in}:${entry.param.name}`, entry);
1173
1308
  return Array.from(paramMap.values());
1174
1309
  }
1310
+ /** The `encoding` of a media type, reduced to the entries that set a field Kubb carries. */
1311
+ function getEncoding(encoding) {
1312
+ const entries = Object.entries(encoding ?? {}).flatMap(([property, { contentType, style, explode, allowReserved }]) => {
1313
+ const node = {
1314
+ ...contentType !== void 0 ? { contentType } : {},
1315
+ ...style !== void 0 ? { style } : {},
1316
+ ...explode !== void 0 ? { explode } : {},
1317
+ ...allowReserved !== void 0 ? { allowReserved } : {}
1318
+ };
1319
+ return Object.keys(node).length ? [[property, node]] : [];
1320
+ });
1321
+ return entries.length ? Object.fromEntries(entries) : void 0;
1322
+ }
1175
1323
  /** Schema for one media type of a `content` map, with the binary fallback for an emptied non-JSON entry. */
1176
1324
  function getBodySchema({ content, contentType, refs }) {
1177
1325
  const entry = contentType ? content?.[contentType] : void 0;
@@ -1279,10 +1427,12 @@ function parseOperation({ operation, ...ctx }) {
1279
1427
  name: requestBodyName
1280
1428
  });
1281
1429
  scanSchema(node, `${pointer}/requestBody/content/${escapePointerToken(ct)}/schema`);
1430
+ const encoding = getEncoding(body?.content?.[ct]?.encoding);
1282
1431
  return [_kubb_kit.ast.factory.createContent({
1283
1432
  contentType: ct,
1284
1433
  schema: _kubb_kit.ast.optionality(node, bodyRequired),
1285
- keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
1434
+ keysToOmit: collectPropertyKeysByFlag(schema, "readOnly"),
1435
+ ...encoding && { encoding }
1286
1436
  })];
1287
1437
  });
1288
1438
  const requestBody = content.length > 0 || body?.description ? {
@@ -1379,6 +1529,42 @@ function createNode({ schema, name, nullable, defaultValue }, extras) {
1379
1529
  //#endregion
1380
1530
  //#region src/emit/discriminator/preserve.ts
1381
1531
  /**
1532
+ * Turns string mapping keys into the discriminator property's type when they parse cleanly.
1533
+ */
1534
+ function typedKeys({ keys, propertyType }) {
1535
+ return keys.map((key) => {
1536
+ if (propertyType === "integer" || propertyType === "number") {
1537
+ const value = Number(key);
1538
+ return key.trim() !== "" && Number.isFinite(value) ? value : key;
1539
+ }
1540
+ if (propertyType === "boolean") {
1541
+ const lower = key.toLowerCase();
1542
+ return lower === "true" ? true : lower === "false" ? false : key;
1543
+ }
1544
+ return key;
1545
+ });
1546
+ }
1547
+ /**
1548
+ * Reads the declared `type` of `propertyName` on a schema, following `$ref`s and `allOf` members.
1549
+ * A nullable array such as `['integer', 'null']` reads as its one non-null type.
1550
+ */
1551
+ function propertyTypeOf({ schema, propertyName, refs, seen = /* @__PURE__ */ new Set() }) {
1552
+ const deref = (value) => value && isReference(value) ? refs.resolve(value.$ref, { report: false }) : value;
1553
+ if (schema && isReference(schema)) {
1554
+ if (seen.has(schema.$ref)) return void 0;
1555
+ seen.add(schema.$ref);
1556
+ }
1557
+ const resolved = deref(schema);
1558
+ const type = deref(resolved?.properties?.[propertyName])?.type ?? resolved?.allOf?.map((member) => propertyTypeOf({
1559
+ schema: member,
1560
+ propertyName,
1561
+ refs,
1562
+ seen
1563
+ })).find(Boolean);
1564
+ const nonNull = Array.isArray(type) ? type.filter((t) => t !== "null") : [];
1565
+ return nonNull.length === 1 ? nonNull[0] : type;
1566
+ }
1567
+ /**
1382
1568
  * Creates a single-property object schema used as a discriminator literal.
1383
1569
  *
1384
1570
  * @example
@@ -1395,7 +1581,7 @@ function createDiscriminantNode({ propertyName, values }) {
1395
1581
  name: propertyName,
1396
1582
  schema: _kubb_kit.ast.factory.createSchema({
1397
1583
  type: "enum",
1398
- primitive: "string",
1584
+ primitive: (0, _kubb_kit.enumPrimitive)(values),
1399
1585
  enumValues: values
1400
1586
  }),
1401
1587
  required: true
@@ -1410,21 +1596,61 @@ function createDiscriminantNode({ propertyName, values }) {
1410
1596
  *
1411
1597
  * @example
1412
1598
  * ```ts
1413
- * findDiscriminators({ dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // ['dog', 'hound']
1599
+ * findDiscriminators({ mapping: { dog: '#/components/schemas/Dog', hound: '#/components/schemas/Dog' }, ref: '#/components/schemas/Dog' }) // ['dog', 'hound']
1414
1600
  * ```
1415
1601
  */
1416
- function findDiscriminators(mapping, ref) {
1602
+ function findDiscriminators({ mapping, ref }) {
1417
1603
  if (!mapping || !ref) return [];
1418
1604
  return Object.entries(mapping).filter(([, value]) => value === ref).map(([key]) => key);
1419
1605
  }
1420
1606
  /**
1607
+ * Finds the discriminator a schema declares itself or through an inline `allOf` member.
1608
+ */
1609
+ function getDiscriminator(schema) {
1610
+ return (isDiscriminator(schema) ? schema : schema.allOf?.find(isDiscriminator))?.discriminator;
1611
+ }
1612
+ /**
1613
+ * Lists the mapping keys of a discriminator plus the keys of the nested discriminators its mapped
1614
+ * schemas declare on the same property, so a root `$type` accepts values from every level.
1615
+ */
1616
+ function getMappingKeys({ discriminator: { propertyName, mapping }, refs, seen = /* @__PURE__ */ new Set() }) {
1617
+ return Object.entries(mapping ?? {}).flatMap(([key, target]) => {
1618
+ if (seen.has(target)) return [key];
1619
+ seen.add(target);
1620
+ const resolved = refs.resolve(target, { report: false });
1621
+ const nested = resolved && getDiscriminator(resolved);
1622
+ return nested?.propertyName === propertyName ? [key, ...getMappingKeys({
1623
+ discriminator: nested,
1624
+ refs,
1625
+ seen
1626
+ })] : [key];
1627
+ });
1628
+ }
1629
+ /**
1630
+ * Drops the discriminator from an inline `allOf` member when a `$ref` sibling already declares one on
1631
+ * the same property. That mapping names the schema's own children, and keeping it pins the property
1632
+ * to those keys next to the parent's, which leaves `never`.
1633
+ */
1634
+ function removeInheritedDiscriminator({ members, refs }) {
1635
+ const inherited = new Set(members.flatMap((member) => {
1636
+ const parent = isReference(member) ? refs.resolve(member.$ref, { report: false }) : null;
1637
+ return parent && getDiscriminator(parent)?.propertyName || [];
1638
+ }));
1639
+ return members.map((member) => {
1640
+ if (isReference(member) || !isDiscriminator(member) || member.oneOf || member.anyOf) return member;
1641
+ if (!inherited.has(member.discriminator.propertyName)) return member;
1642
+ const { discriminator: _discriminator, ...rest } = member;
1643
+ return rest;
1644
+ });
1645
+ }
1646
+ /**
1421
1647
  * Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
1422
1648
  * node with either the shared-properties slice carrying that value, or a synthetic discriminant
1423
1649
  * literal. The referenced child schema's own definition is left untouched — narrowing happens only
1424
1650
  * at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
1425
1651
  * which additionally patches the child schema's own definition in a post-pass).
1426
1652
  */
1427
- function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, name, refs }) {
1653
+ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parentType, parse, name, refs }) {
1428
1654
  function pickDiscriminatorPropertyNode(node, propertyName) {
1429
1655
  const discriminatorProperty = _kubb_kit.ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
1430
1656
  if (!discriminatorProperty) return null;
@@ -1460,9 +1686,20 @@ function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode,
1460
1686
  }
1461
1687
  return unionMembers.map((s) => {
1462
1688
  const ref = isReference(s) ? s.$ref : void 0;
1463
- const mappedValues = findDiscriminators(discriminator?.mapping, ref);
1689
+ const mappedValues = findDiscriminators({
1690
+ mapping: discriminator?.mapping,
1691
+ ref
1692
+ });
1464
1693
  const implicitValue = mappedValues.length ? null : implicitDiscriminantValue(s);
1465
- const discriminatorValues = mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [];
1694
+ const propertyType = propertyTypeOf({
1695
+ schema: s,
1696
+ propertyName: discriminator?.propertyName ?? "",
1697
+ refs
1698
+ }) ?? parentType;
1699
+ const discriminatorValues = typedKeys({
1700
+ keys: mappedValues.length ? mappedValues : implicitValue ? [implicitValue] : [],
1701
+ propertyType
1702
+ });
1466
1703
  const memberNode = parse({
1467
1704
  schema: s,
1468
1705
  name
@@ -1494,21 +1731,34 @@ function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
1494
1731
  members: allOfMembers.filter((item) => {
1495
1732
  if (!isReference(item) || !name) return true;
1496
1733
  const deref = refs.resolve(item.$ref);
1497
- if (!deref || !isDiscriminator(deref)) return true;
1498
- const parentUnion = deref.oneOf ?? deref.anyOf;
1499
- if (!parentUnion) return true;
1734
+ const discriminator = deref && getDiscriminator(deref);
1735
+ const parentUnion = deref?.oneOf ?? deref?.anyOf;
1736
+ if (!deref || !discriminator || isDiscriminator(deref) && !parentUnion) return true;
1500
1737
  const childRef = `${SCHEMA_REF_PREFIX}${name}`;
1501
- const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
1502
- const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
1503
- if (inOneOf || inMapping) {
1504
- const values = findDiscriminators(deref.discriminator.mapping, childRef);
1505
- if (values.length) discriminantValues.push({
1506
- propertyName: deref.discriminator.propertyName,
1507
- values
1738
+ const values = findDiscriminators({
1739
+ mapping: discriminator.mapping,
1740
+ ref: childRef
1741
+ });
1742
+ if (values.length) {
1743
+ const { propertyName } = discriminator;
1744
+ const propertyType = propertyTypeOf({
1745
+ schema: deref,
1746
+ propertyName,
1747
+ refs
1748
+ }) ?? propertyTypeOf({
1749
+ schema: { $ref: childRef },
1750
+ propertyName,
1751
+ refs
1752
+ });
1753
+ discriminantValues.push({
1754
+ propertyName,
1755
+ values: typedKeys({
1756
+ keys: values,
1757
+ propertyType
1758
+ })
1508
1759
  });
1509
- return false;
1510
1760
  }
1511
- return true;
1761
+ return !(parentUnion && (values.length > 0 || parentUnion.some((member) => isReference(member) && member.$ref === childRef)));
1512
1762
  }),
1513
1763
  discriminantValues
1514
1764
  };
@@ -1635,10 +1885,9 @@ function resolveUnionMembers(members, siblingProperties) {
1635
1885
  });
1636
1886
  }
1637
1887
  /**
1638
- * Enriches any `oneOf` or `anyOf` branches on a schema object with properties from sibling schemas.
1888
+ * Enriches a schema object, and any `oneOf` or `anyOf` branches on it, with properties from sibling schemas.
1639
1889
  */
1640
1890
  function resolveUnionSchema(schema, siblingProperties) {
1641
- if (!schema.oneOf && !schema.anyOf) return schema;
1642
1891
  const next = { ...resolveUnionMembers([schema], siblingProperties)[0] ?? schema };
1643
1892
  if (schema.oneOf) next.oneOf = resolveUnionMembers(schema.oneOf, siblingProperties);
1644
1893
  if (schema.anyOf) next.anyOf = resolveUnionMembers(schema.anyOf, siblingProperties);
@@ -1662,6 +1911,17 @@ function convertRef({ schema, name, nullable, defaultValue, document, parse, ref
1662
1911
  defaultValue
1663
1912
  };
1664
1913
  if (refPath && document.components && !refs.resolve(refPath, { report: false })) return createNode(ctx, { type: "unknown" });
1914
+ if (refPath && !refPath.startsWith("#/components/schemas/") && resolvedSchema) return _kubb_kit.ast.factory.createSchema({
1915
+ ...resolvedSchema,
1916
+ title: schema.title ?? resolvedSchema.title,
1917
+ description: schema.description ?? resolvedSchema.description,
1918
+ deprecated: schema.deprecated ?? resolvedSchema.deprecated,
1919
+ readOnly: schema.readOnly ?? resolvedSchema.readOnly,
1920
+ writeOnly: schema.writeOnly ?? resolvedSchema.writeOnly,
1921
+ default: defaultValue ?? resolvedSchema.default,
1922
+ name: name ?? resolvedSchema.name,
1923
+ nullable: nullable ?? resolvedSchema.nullable
1924
+ });
1665
1925
  const targetName = renames?.get(schema.$ref);
1666
1926
  return createNode(ctx, {
1667
1927
  type: "ref",
@@ -1693,9 +1953,41 @@ function convertNot({ schema, name, nullable, defaultValue, parse }) {
1693
1953
  });
1694
1954
  }
1695
1955
  /**
1956
+ * Keywords that describe a schema or conditionally constrain it, but add no structure.
1957
+ */
1958
+ const annotationKeys = /* @__PURE__ */ new Set([
1959
+ "description",
1960
+ "title",
1961
+ "example",
1962
+ "examples",
1963
+ "default",
1964
+ "deprecated",
1965
+ "readOnly",
1966
+ "writeOnly",
1967
+ "if",
1968
+ "then",
1969
+ "else"
1970
+ ]);
1971
+ /**
1972
+ * Moves `allOf` members that hold only annotations or `if`/`then`/`else` onto the parent, so they do
1973
+ * not become `unknown`. The parent's own values win, and the conditional keywords are not enforced.
1974
+ */
1975
+ function mergeAnnotations(schema) {
1976
+ const members = schema.allOf;
1977
+ const isAnnotation = (member) => !isReference(member) && Object.keys(member).every((key) => annotationKeys.has(key));
1978
+ const annotations = members.filter(isAnnotation);
1979
+ const rest = members.filter((member) => !isAnnotation(member));
1980
+ return annotations.length && rest.length ? {
1981
+ ...Object.assign({}, ...annotations),
1982
+ ...schema,
1983
+ allOf: rest
1984
+ } : schema;
1985
+ }
1986
+ /**
1696
1987
  * Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
1697
1988
  */
1698
- function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1989
+ function convertAllOf({ schema: original, name, nullable, defaultValue, parse, refs }) {
1990
+ const schema = mergeAnnotations(original);
1699
1991
  if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
1700
1992
  const [memberSchema] = schema.allOf;
1701
1993
  const memberNode = parse({
@@ -1721,7 +2013,10 @@ function convertAllOf({ schema, name, nullable, defaultValue, parse, refs }) {
1721
2013
  });
1722
2014
  }
1723
2015
  const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
1724
- allOfMembers: schema.allOf,
2016
+ allOfMembers: removeInheritedDiscriminator({
2017
+ members: schema.allOf,
2018
+ refs
2019
+ }),
1725
2020
  name,
1726
2021
  refs
1727
2022
  });
@@ -1801,6 +2096,11 @@ function convertUnion(context) {
1801
2096
  unionMembers,
1802
2097
  discriminator,
1803
2098
  sharedPropertiesNode,
2099
+ parentType: discriminator && propertyTypeOf({
2100
+ schema,
2101
+ propertyName: discriminator.propertyName,
2102
+ refs
2103
+ }),
1804
2104
  parse,
1805
2105
  name,
1806
2106
  refs
@@ -2064,7 +2364,7 @@ function nameEnums(node, options) {
2064
2364
  /**
2065
2365
  * Converts an object-like schema into an `ObjectSchemaNode`.
2066
2366
  */
2067
- function convertObject({ schema, name, nullable, defaultValue, options, parse }) {
2367
+ function convertObject({ schema, name, nullable, defaultValue, options, parse, refs }) {
2068
2368
  const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
2069
2369
  const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
2070
2370
  const resolvedPropSchema = propSchema;
@@ -2081,7 +2381,7 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2081
2381
  name: propName,
2082
2382
  schema: {
2083
2383
  ...schemaNode,
2084
- nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
2384
+ nullable: schemaNode.type === "null" ? void 0 : propNullable || schemaNode.nullable || void 0
2085
2385
  },
2086
2386
  required
2087
2387
  });
@@ -2121,7 +2421,17 @@ function convertObject({ schema, name, nullable, defaultValue, options, parse })
2121
2421
  });
2122
2422
  if (isDiscriminator(schema) && schema.discriminator.mapping) {
2123
2423
  const discPropName = schema.discriminator.propertyName;
2124
- const values = Object.keys(schema.discriminator.mapping);
2424
+ const values = typedKeys({
2425
+ keys: getMappingKeys({
2426
+ discriminator: schema.discriminator,
2427
+ refs
2428
+ }),
2429
+ propertyType: propertyTypeOf({
2430
+ schema,
2431
+ propertyName: discPropName,
2432
+ refs
2433
+ })
2434
+ });
2125
2435
  const enumName = name ? (0, _kubb_kit.enumPropName)(name, discPropName, options.enumSuffix) : void 0;
2126
2436
  return _kubb_kit.ast.applyMacros(objectNode, [(0, _kubb_kit.macroDiscriminatorEnum)({
2127
2437
  propertyName: discPropName,
@@ -2179,8 +2489,9 @@ function convertArray({ schema, name, nullable, defaultValue, options, parse })
2179
2489
  //#region src/emit/parseSchema.ts
2180
2490
  /**
2181
2491
  * Ordered schema rule table. Order is significant: composition keywords (`not`, `$ref`, `allOf`,
2182
- * `oneOf`/`anyOf`) take precedence over `const`, which takes precedence over a genuine OAS 3.1
2183
- * multi-type array (more than one non-`null` type), which takes precedence over `format`/`type`.
2492
+ * `oneOf`/`anyOf`) take precedence over `const` (except a `$ref` with a sibling `const`, where the
2493
+ * `const` wins), which takes precedence over a genuine OAS 3.1 multi-type array (more than one
2494
+ * non-`null` type), which takes precedence over `format`/`type`.
2184
2495
  * A multi-type array must split into its per-type members before `format` runs, otherwise
2185
2496
  * `format` collapses the whole schema to one type (e.g. `type: ['null', 'integer', 'string'],
2186
2497
  * format: 'int32'` would drop `string` and emit just `integer`); each split-off member still
@@ -2194,7 +2505,7 @@ const schemaRules = [
2194
2505
  convert: convertNot
2195
2506
  },
2196
2507
  {
2197
- match: ({ schema }) => isReference(schema),
2508
+ match: ({ schema }) => isReference(schema) && !("const" in schema && schema.const !== void 0),
2198
2509
  convert: convertRef
2199
2510
  },
2200
2511
  {
@@ -2362,114 +2673,6 @@ function refPromotedEnums(node, promoted) {
2362
2673
  } });
2363
2674
  }
2364
2675
  //#endregion
2365
- //#region src/refs.ts
2366
- /**
2367
- * Creates the `$ref` resolution service for one document. One pointer walk backs every method.
2368
- *
2369
- * @example
2370
- * ```ts
2371
- * const refs = createRefs(document)
2372
- * refs.resolve<SchemaObject>('#/components/schemas/Pet')
2373
- * refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
2374
- * refs.resolveNode('#/components/schemas/Pet', parseSchema)
2375
- * refs.deref<ResponseObject>(operation.schema.responses?.['200'])
2376
- * refs.derefKeepingRef(parameter)
2377
- * ```
2378
- */
2379
- function createRefs(document) {
2380
- const pointerCache = /* @__PURE__ */ new Map();
2381
- const resolvedNodeCache = /* @__PURE__ */ new Map();
2382
- const resolvingRefs = /* @__PURE__ */ new Set();
2383
- function walkPointer($ref) {
2384
- const trimmed = $ref.trim();
2385
- if (trimmed === "" || !trimmed.startsWith("#")) return {
2386
- applicable: false,
2387
- value: null
2388
- };
2389
- const pointer = trimmed.substring(1);
2390
- if (pointerCache.has(pointer)) return {
2391
- applicable: true,
2392
- value: pointerCache.get(pointer)
2393
- };
2394
- const current = pointer.split("/").filter(Boolean).map((token) => globalThis.decodeURIComponent(token).replaceAll("~1", "/").replaceAll("~0", "~")).reduce((obj, key) => obj?.[key], document);
2395
- if (current) pointerCache.set(pointer, current);
2396
- return {
2397
- applicable: true,
2398
- value: current ?? null
2399
- };
2400
- }
2401
- /**
2402
- * Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
2403
- * `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
2404
- * outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
2405
- * for a speculative lookup where a missing ref is not an error.
2406
- */
2407
- function resolve(refPath, options) {
2408
- const { applicable, value } = walkPointer(refPath);
2409
- if (!applicable || value || options?.report === false) return value;
2410
- const diagnostic = {
2411
- code: _kubb_kit.Diagnostics.code.refNotFound,
2412
- severity: "error",
2413
- message: `Could not find a definition for ${refPath}.`,
2414
- help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
2415
- location: {
2416
- kind: "schema",
2417
- pointer: refPath,
2418
- ref: refPath
2419
- }
2420
- };
2421
- if (!_kubb_kit.Diagnostics.report(diagnostic)) throw new _kubb_kit.Diagnostics.Error(diagnostic);
2422
- return null;
2423
- }
2424
- /**
2425
- * Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
2426
- * instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
2427
- * resolved (e.g. a minimal document in a unit test).
2428
- */
2429
- function resolveNode(refPath, parse) {
2430
- if (resolvingRefs.has(refPath)) return null;
2431
- if (!resolvedNodeCache.has(refPath)) {
2432
- let resolved = null;
2433
- try {
2434
- const referenced = resolve(refPath);
2435
- if (referenced) {
2436
- resolvingRefs.add(refPath);
2437
- resolved = parse({ schema: referenced });
2438
- resolvingRefs.delete(refPath);
2439
- }
2440
- } catch {}
2441
- resolvedNodeCache.set(refPath, resolved);
2442
- }
2443
- return resolvedNodeCache.get(refPath) ?? null;
2444
- }
2445
- /**
2446
- * Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
2447
- * resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
2448
- * `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
2449
- * returned as-is.
2450
- */
2451
- function deref(value) {
2452
- if (!isReference(value)) return value ? value : null;
2453
- const resolved = resolve(value.$ref);
2454
- return resolved && !isReference(resolved) ? resolved : null;
2455
- }
2456
- /** Resolves a `$ref` object but keeps the `$ref` field on the result; other values pass through. */
2457
- function derefKeepingRef(value) {
2458
- if (isReference(value)) return {
2459
- ...value,
2460
- ...resolve(value.$ref),
2461
- $ref: value.$ref
2462
- };
2463
- return value;
2464
- }
2465
- return {
2466
- resolve,
2467
- resolveNode,
2468
- deref,
2469
- derefKeepingRef
2470
- };
2471
- }
2472
- //#endregion
2473
2676
  //#region src/sourceLocator.ts
2474
2677
  let cached;
2475
2678
  const toRange = ([start, end]) => ({
@@ -2702,7 +2905,7 @@ const adapterOas = (0, _kubb_core.defineExtension)({
2702
2905
  };
2703
2906
  parsedByName.set(name, node);
2704
2907
  scanSchema(node);
2705
- if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
2908
+ if (node.type === "ref" && node.name) refAliasMap.set(name, node);
2706
2909
  if (_kubb_kit.ast.narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
2707
2910
  if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
2708
2911
  }
@@ -2717,6 +2920,32 @@ const adapterOas = (0, _kubb_core.defineExtension)({
2717
2920
  }));
2718
2921
  const promotedEnums = enums === "root" ? collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas))) : null;
2719
2922
  if (promotedEnums) for (const name of promotedEnums.keys()) enumNames.push(name);
2923
+ function isAliasCycle(name) {
2924
+ const seen = /* @__PURE__ */ new Set([name]);
2925
+ for (let next = refAliasMap.get(name)?.name; next; next = refAliasMap.get(next)?.name) {
2926
+ if (next === name) return true;
2927
+ if (seen.has(next)) return false;
2928
+ seen.add(next);
2929
+ }
2930
+ return false;
2931
+ }
2932
+ function createCycleNode(name, pointer) {
2933
+ _kubb_kit.Diagnostics.report({
2934
+ code: _kubb_kit.Diagnostics.code.aliasCycle,
2935
+ severity: "warning",
2936
+ message: `${name} only refers to other schemas that lead back to it, so it is resolved to unknown.`,
2937
+ help: "Give one of the schemas in the loop a real definition, or remove the `$ref` that closes it.",
2938
+ location: {
2939
+ kind: "schema",
2940
+ pointer: pointer ?? `#/components/schemas/${name}`
2941
+ }
2942
+ });
2943
+ return _kubb_kit.ast.factory.createSchema({
2944
+ type: "unknown",
2945
+ name,
2946
+ pointer
2947
+ });
2948
+ }
2720
2949
  const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
2721
2950
  for (const name of Object.keys(schemas)) {
2722
2951
  const aliasTarget = refAliasMap.get(name)?.name;
@@ -2724,11 +2953,12 @@ const adapterOas = (0, _kubb_core.defineExtension)({
2724
2953
  const parsed = parsedByName.get(name);
2725
2954
  const child = discriminatorChildMap?.get(name);
2726
2955
  const base = child ? patchDiscriminatorNode(parsed, child) : parsed;
2727
- const node = aliased ? {
2956
+ const resolved = aliased ? {
2728
2957
  ...aliased,
2729
2958
  name,
2730
2959
  pointer: parsed.pointer
2731
2960
  } : base;
2961
+ const node = isAliasCycle(name) ? createCycleNode(name, parsed.pointer) : resolved;
2732
2962
  schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
2733
2963
  }
2734
2964
  const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;