schema-components 1.19.0 → 1.21.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.
Files changed (70) hide show
  1. package/dist/core/adapter.d.mts +10 -3
  2. package/dist/core/adapter.mjs +237 -31
  3. package/dist/core/constraints.d.mts +2 -2
  4. package/dist/core/constraints.mjs +0 -2
  5. package/dist/core/diagnostics.d.mts +1 -1
  6. package/dist/core/errors.d.mts +1 -1
  7. package/dist/core/errors.mjs +10 -8
  8. package/dist/core/fieldOrder.d.mts +1 -1
  9. package/dist/core/formats.d.mts +21 -14
  10. package/dist/core/formats.mjs +88 -4
  11. package/dist/core/merge.d.mts +11 -2
  12. package/dist/core/merge.mjs +11 -0
  13. package/dist/core/normalise.d.mts +9 -3
  14. package/dist/core/normalise.mjs +1 -1
  15. package/dist/core/openapi30.d.mts +24 -1
  16. package/dist/core/openapi30.mjs +2 -2
  17. package/dist/core/ref.d.mts +1 -1
  18. package/dist/core/ref.mjs +34 -9
  19. package/dist/core/renderer.d.mts +1 -1
  20. package/dist/core/swagger2.d.mts +1 -1
  21. package/dist/core/swagger2.mjs +1 -1
  22. package/dist/core/typeInference.d.mts +2 -2
  23. package/dist/core/types.d.mts +1 -1
  24. package/dist/core/uri.d.mts +41 -0
  25. package/dist/core/uri.mjs +76 -0
  26. package/dist/core/version.d.mts +2 -2
  27. package/dist/core/version.mjs +25 -1
  28. package/dist/core/walkBuilders.d.mts +13 -5
  29. package/dist/core/walkBuilders.mjs +11 -3
  30. package/dist/core/walker.d.mts +1 -1
  31. package/dist/core/walker.mjs +80 -26
  32. package/dist/{diagnostics-VgEKI_Ct.d.mts → diagnostics-CbBPsxSt.d.mts} +1 -1
  33. package/dist/{errors-CnGjT1cg.d.mts → errors-QEwOtQAA.d.mts} +8 -5
  34. package/dist/html/a11y.d.mts +2 -2
  35. package/dist/html/renderToHtml.d.mts +2 -2
  36. package/dist/html/renderToHtmlStream.d.mts +2 -2
  37. package/dist/html/renderers.d.mts +2 -2
  38. package/dist/html/renderers.mjs +9 -2
  39. package/dist/html/streamRenderers.d.mts +2 -2
  40. package/dist/{normalise-C0ofw3W6.mjs → normalise-DaSrnr8g.mjs} +574 -40
  41. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  42. package/dist/openapi/ApiLinks.d.mts +1 -1
  43. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  44. package/dist/openapi/ApiSecurity.d.mts +1 -1
  45. package/dist/openapi/ApiSecurity.mjs +113 -7
  46. package/dist/openapi/bundle.mjs +2 -0
  47. package/dist/openapi/components.d.mts +32 -10
  48. package/dist/openapi/components.mjs +37 -16
  49. package/dist/openapi/parser.d.mts +1 -1
  50. package/dist/openapi/parser.mjs +41 -4
  51. package/dist/openapi/resolve.d.mts +70 -9
  52. package/dist/openapi/resolve.mjs +124 -24
  53. package/dist/react/SchemaComponent.d.mts +21 -9
  54. package/dist/react/SchemaComponent.mjs +32 -4
  55. package/dist/react/SchemaView.d.mts +3 -3
  56. package/dist/react/fieldPath.d.mts +1 -1
  57. package/dist/react/headless.d.mts +1 -1
  58. package/dist/react/headlessRenderers.d.mts +2 -2
  59. package/dist/react/headlessRenderers.mjs +18 -6
  60. package/dist/{ref-Bb43ZURY.d.mts → ref-si8ViYun.d.mts} +7 -2
  61. package/dist/{renderer-BQqiXUYP.d.mts → renderer-DI6ZYf7a.d.mts} +1 -1
  62. package/dist/themes/mantine.d.mts +1 -1
  63. package/dist/themes/mui.d.mts +1 -1
  64. package/dist/themes/radix.d.mts +1 -1
  65. package/dist/themes/shadcn.d.mts +1 -1
  66. package/dist/typeInference-Bxw3NOG1.d.mts +647 -0
  67. package/dist/{types-D_5ST7SS.d.mts → types-BnxPEElk.d.mts} +18 -2
  68. package/dist/{version-XNH7PRGP.d.mts → version-D-u7aMfy.d.mts} +36 -1
  69. package/package.json +1 -1
  70. package/dist/typeInference-5JiqIZ8t.d.mts +0 -388
@@ -1,6 +1,6 @@
1
1
  import { isObject } from "./core/guards.mjs";
2
2
  import { appendPointer, emitDiagnostic } from "./core/diagnostics.mjs";
3
- import { isOpenApi30, isSwagger2 } from "./core/version.mjs";
3
+ import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./core/version.mjs";
4
4
  //#region src/core/openapi30.ts
5
5
  /**
6
6
  * OpenAPI 3.0.x schema normalisation.
@@ -117,6 +117,260 @@ function normaliseOpenApi30Discriminator(node) {
117
117
  return node;
118
118
  }
119
119
  /**
120
+ * Returns the schema name a `$ref` points at when it targets
121
+ * `#/components/schemas/<Name>`, or `undefined` otherwise.
122
+ *
123
+ * The walker only resolves intra-document refs and other allOf-base
124
+ * patterns; refs into `definitions` (Swagger 2.0) are already rewritten
125
+ * before this stage.
126
+ */
127
+ function componentSchemaName(ref) {
128
+ if (typeof ref !== "string") return void 0;
129
+ if (!ref.startsWith("#/components/schemas/")) return void 0;
130
+ const name = ref.slice(21);
131
+ return name.length > 0 ? name : void 0;
132
+ }
133
+ /**
134
+ * Find every immediate `$ref` that an `allOf` array contains pointing
135
+ * back at a `components/schemas/<Name>` entry. Used to discover
136
+ * "Cat extends Pet"-style inheritance — the subtype's `allOf` lists
137
+ * the base by `$ref` alongside its own local fields.
138
+ */
139
+ function listAllOfBaseRefs(schema) {
140
+ const allOf = schema.allOf;
141
+ if (!Array.isArray(allOf)) return [];
142
+ const result = [];
143
+ for (const entry of allOf) {
144
+ if (!isObject(entry)) continue;
145
+ const name = componentSchemaName(entry.$ref);
146
+ if (name !== void 0) result.push(name);
147
+ }
148
+ return result;
149
+ }
150
+ /**
151
+ * Collect discriminator subtypes for a base schema. Entries come from:
152
+ *
153
+ * 1. The base's `discriminator.mapping` (explicit author intent — the
154
+ * mapping key supplies the `const` value, the ref names the subtype).
155
+ * 2. Component schemas whose `allOf` lists this base by `$ref` and
156
+ * were not already named in the mapping. The `const` value defaults
157
+ * to the subtype's component name.
158
+ *
159
+ * Returned in deterministic order: mapping entries first (preserving
160
+ * authored order), then implicit subtypes alphabetically.
161
+ */
162
+ function collectDiscriminatorSubtypes(baseName, discriminator, componentSchemas) {
163
+ const result = [];
164
+ const seen = /* @__PURE__ */ new Set();
165
+ const mapping = isObject(discriminator.mapping) ? discriminator.mapping : void 0;
166
+ if (mapping !== void 0) for (const [constValue, ref] of Object.entries(mapping)) {
167
+ const name = componentSchemaName(ref);
168
+ if (name === void 0) continue;
169
+ if (!isObject(componentSchemas[name])) continue;
170
+ if (seen.has(name)) continue;
171
+ seen.add(name);
172
+ result.push({
173
+ name,
174
+ constValue
175
+ });
176
+ }
177
+ const implicitNames = [];
178
+ for (const [name, schema] of Object.entries(componentSchemas)) {
179
+ if (!isObject(schema)) continue;
180
+ if (seen.has(name)) continue;
181
+ if (!listAllOfBaseRefs(schema).includes(baseName)) continue;
182
+ implicitNames.push(name);
183
+ }
184
+ implicitNames.sort();
185
+ for (const name of implicitNames) {
186
+ result.push({
187
+ name,
188
+ constValue: name
189
+ });
190
+ seen.add(name);
191
+ }
192
+ return result;
193
+ }
194
+ /**
195
+ * Inject the discriminator `const` on a subtype schema in-place.
196
+ *
197
+ * When the subtype already declares a matching const we leave it
198
+ * alone. Otherwise the const is added in whichever location the walker
199
+ * will actually observe:
200
+ *
201
+ * - Subtype declares `allOf`: append a new `allOf` entry carrying just
202
+ * `{ properties: { [propertyName]: { const } } }`. The walker's
203
+ * `mergeAllOf` merges every entry's `properties` into the resolved
204
+ * schema, so the const propagates through to the merged result. A
205
+ * top-level `properties` sibling of `allOf` would be ignored by the
206
+ * merge.
207
+ * - Subtype does not declare `allOf`: extend the top-level `properties`
208
+ * block — the walker reads this directly.
209
+ */
210
+ function injectSubtypeConst(subtype, propertyName, constValue) {
211
+ if (subtypeAlreadyDeclaresConst(subtype, propertyName)) return;
212
+ const constEntry = { properties: { [propertyName]: { const: constValue } } };
213
+ if (Array.isArray(subtype.allOf)) {
214
+ subtype.allOf = [...subtype.allOf, constEntry];
215
+ return;
216
+ }
217
+ const existingProps = isObject(subtype.properties) ? { ...subtype.properties } : {};
218
+ const existingDisc = existingProps[propertyName];
219
+ existingProps[propertyName] = {
220
+ ...isObject(existingDisc) ? existingDisc : {},
221
+ const: constValue
222
+ };
223
+ subtype.properties = existingProps;
224
+ }
225
+ /**
226
+ * Check whether a subtype (or any of its `allOf` entries) already
227
+ * carries a `const` for the discriminator property. Used to avoid
228
+ * overwriting an author-supplied const.
229
+ */
230
+ function subtypeAlreadyDeclaresConst(subtype, propertyName) {
231
+ if (hasConstProp(subtype.properties, propertyName)) return true;
232
+ if (Array.isArray(subtype.allOf)) for (const entry of subtype.allOf) {
233
+ if (!isObject(entry)) continue;
234
+ if (hasConstProp(entry.properties, propertyName)) return true;
235
+ }
236
+ return false;
237
+ }
238
+ function hasConstProp(properties, propertyName) {
239
+ if (!isObject(properties)) return false;
240
+ const prop = properties[propertyName];
241
+ return isObject(prop) && "const" in prop;
242
+ }
243
+ /**
244
+ * Strip every `$ref` entry in a subtype's `allOf` that targets the
245
+ * discriminator base. The base's own schema content (properties,
246
+ * required, type) is replicated into a synthesised `allOf` entry so
247
+ * the subtype remains structurally complete — without this the base's
248
+ * synthesised `oneOf` would cycle through the subtype's `$ref`s on
249
+ * every walk (Dog → Pet.oneOf → Dog → ...).
250
+ */
251
+ function rewriteSubtypeAllOf(subtype, baseName, baseInherited) {
252
+ const allOf = subtype.allOf;
253
+ if (!Array.isArray(allOf)) return;
254
+ const baseRefPrefix = `#/components/schemas/${baseName}`;
255
+ const rewritten = [];
256
+ let removedBaseRef = false;
257
+ for (const entry of allOf) {
258
+ if (isObject(entry) && typeof entry.$ref === "string" && entry.$ref === baseRefPrefix) {
259
+ removedBaseRef = true;
260
+ continue;
261
+ }
262
+ rewritten.push(entry);
263
+ }
264
+ if (!removedBaseRef) return;
265
+ subtype.allOf = [baseInherited, ...rewritten];
266
+ }
267
+ /**
268
+ * Capture the "inheritable" portion of a base schema before rewriting:
269
+ * `properties`, `required`, `type`, and any other constraint that
270
+ * subtypes used to inherit through `$ref`. The discriminator keyword
271
+ * and the synthesised `oneOf` are intentionally excluded — subtypes
272
+ * never inherited those and including them would re-introduce the
273
+ * Pet → Dog → Pet cycle.
274
+ */
275
+ function captureBaseInherited(base) {
276
+ const inherited = {};
277
+ for (const [key, value] of Object.entries(base)) {
278
+ if (key === "discriminator") continue;
279
+ if (key === "oneOf") continue;
280
+ if (key === "anyOf") continue;
281
+ inherited[key] = value;
282
+ }
283
+ return inherited;
284
+ }
285
+ /**
286
+ * Build an inline `oneOf` option that targets a subtype via `$ref` and
287
+ * carries the discriminator property's `const` at the option's top
288
+ * level. The const sibling is what makes `detectDiscriminated` classify
289
+ * the parent `oneOf` as a discriminated union — it inspects each
290
+ * option's literal `properties`, not the resolved schema.
291
+ */
292
+ function buildDiscriminatorOption(subtype, propertyName) {
293
+ return {
294
+ $ref: `#/components/schemas/${subtype.name}`,
295
+ properties: { [propertyName]: { const: subtype.constValue } }
296
+ };
297
+ }
298
+ /**
299
+ * Document-level pre-pass for OpenAPI discriminators that are declared
300
+ * on a base schema and inherited by subtypes via `allOf`.
301
+ *
302
+ * The per-node {@link normaliseOpenApi30Discriminator} only handles
303
+ * discriminators that already sit alongside `oneOf`/`anyOf`. For the
304
+ * canonical "Cat extends Pet" pattern — where `Pet` carries the
305
+ * discriminator and `Cat`/`Dog` reference `Pet` via `allOf` — the
306
+ * discriminator is silently lost. This pre-pass:
307
+ *
308
+ * 1. Injects the discriminator `const` on each subtype's local
309
+ * `properties` (so a direct render of the subtype validates the
310
+ * discriminator value correctly).
311
+ * 2. Synthesises a `oneOf` on the base whenever it lacks one, listing
312
+ * each subtype as `{ $ref, properties: { propertyName: { const } } }`.
313
+ * The per-node discriminator transform then sees `oneOf` and clears
314
+ * the `discriminator` keyword, and the walker's
315
+ * `detectDiscriminated` finds the per-option `const`s.
316
+ *
317
+ * Mutates a shallow clone of `components/schemas` — the input document
318
+ * is never modified.
319
+ */
320
+ function applyDiscriminatorAllOfPrepass(doc) {
321
+ const components = doc.components;
322
+ if (!isObject(components)) return doc;
323
+ const schemas = components.schemas;
324
+ if (!isObject(schemas)) return doc;
325
+ const plans = [];
326
+ for (const [baseName, base] of Object.entries(schemas)) {
327
+ if (!isObject(base)) continue;
328
+ const discriminator = base.discriminator;
329
+ if (!isObject(discriminator)) continue;
330
+ const propertyName = discriminator.propertyName;
331
+ if (typeof propertyName !== "string") continue;
332
+ const subtypes = collectDiscriminatorSubtypes(baseName, discriminator, schemas);
333
+ if (subtypes.length === 0) continue;
334
+ plans.push({
335
+ baseName,
336
+ propertyName,
337
+ subtypes,
338
+ baseHasOneOfOrAnyOf: Array.isArray(base.oneOf) || Array.isArray(base.anyOf),
339
+ baseInherited: captureBaseInherited(base)
340
+ });
341
+ }
342
+ if (plans.length === 0) return doc;
343
+ const newSchemas = { ...schemas };
344
+ const cloneSchema = (name) => {
345
+ const existing = newSchemas[name];
346
+ if (!isObject(existing)) throw new Error(`applyDiscriminatorAllOfPrepass: schema "${name}" disappeared between planning and rewrite`);
347
+ const clone = { ...existing };
348
+ newSchemas[name] = clone;
349
+ return clone;
350
+ };
351
+ for (const plan of plans) {
352
+ for (const subtype of plan.subtypes) {
353
+ const clone = cloneSchema(subtype.name);
354
+ rewriteSubtypeAllOf(clone, plan.baseName, plan.baseInherited);
355
+ injectSubtypeConst(clone, plan.propertyName, subtype.constValue);
356
+ }
357
+ if (!plan.baseHasOneOfOrAnyOf) {
358
+ const baseClone = cloneSchema(plan.baseName);
359
+ baseClone.oneOf = plan.subtypes.map((subtype) => buildDiscriminatorOption(subtype, plan.propertyName));
360
+ delete baseClone.properties;
361
+ delete baseClone.required;
362
+ delete baseClone.type;
363
+ }
364
+ }
365
+ return {
366
+ ...doc,
367
+ components: {
368
+ ...components,
369
+ schemas: newSchemas
370
+ }
371
+ };
372
+ }
373
+ /**
120
374
  * Combined OpenAPI 3.0.x node transform: Draft 04 + nullable + discriminator.
121
375
  * Applied to every schema node in an OpenAPI 3.0 document.
122
376
  *
@@ -278,7 +532,7 @@ function normaliseContentMap(content, normaliseSchema) {
278
532
  const encoding = mediaObj.encoding;
279
533
  if (isObject(encoding)) normalised.encoding = mapObjectValues(encoding, (enc) => isObject(enc) ? normaliseEncoding(enc, normaliseSchema) : enc);
280
534
  if ("example" in normalised && !("examples" in normalised)) {
281
- normalised.examples = { value: normalised.example };
535
+ normalised.examples = { default: { value: normalised.example } };
282
536
  delete normalised.example;
283
537
  } else if ("example" in normalised) delete normalised.example;
284
538
  result[mediaType] = normalised;
@@ -332,7 +586,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
332
586
  result.servers = [{ url: `${typeof schemes[0] === "string" ? schemes[0] : "https"}://${host}${basePath}` }];
333
587
  }
334
588
  const paths = doc.paths;
335
- if (isObject(paths)) result.paths = normaliseSwaggerPaths(paths, doc);
589
+ if (isObject(paths)) result.paths = normaliseSwaggerPaths(paths, doc, diagnostics);
336
590
  const components = {};
337
591
  const definitions = doc.definitions;
338
592
  if (isObject(definitions)) {
@@ -353,7 +607,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
353
607
  const resolved = resolveSwaggerParameter(param, doc);
354
608
  const location = resolved.in;
355
609
  if (location === "body") requestBodies[name] = buildRequestBody(resolved, globalConsumes);
356
- else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), ["multipart/form-data"]);
610
+ else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
357
611
  else convertedParameters[name] = normaliseSwaggerParameter(resolved, doc);
358
612
  }
359
613
  if (Object.keys(convertedParameters).length > 0) components.parameters = convertedParameters;
@@ -381,7 +635,7 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
381
635
  });
382
636
  return result;
383
637
  }
384
- function normaliseSwaggerPaths(paths, doc) {
638
+ function normaliseSwaggerPaths(paths, doc, diagnostics) {
385
639
  const result = {};
386
640
  const METHODS = [
387
641
  "get",
@@ -401,7 +655,7 @@ function normaliseSwaggerPaths(paths, doc) {
401
655
  for (const method of METHODS) {
402
656
  const operation = pathItem[method];
403
657
  if (!isObject(operation)) continue;
404
- normalisedPath[method] = normaliseSwaggerOperation(operation, doc);
658
+ normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
405
659
  }
406
660
  const pathParams = pathItem.parameters;
407
661
  if (Array.isArray(pathParams)) normalisedPath.parameters = pathParams.map((p) => isObject(p) ? normaliseSwaggerParameter(p, doc) : p);
@@ -409,7 +663,7 @@ function normaliseSwaggerPaths(paths, doc) {
409
663
  }
410
664
  return result;
411
665
  }
412
- function normaliseSwaggerOperation(operation, doc) {
666
+ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
413
667
  const result = {};
414
668
  const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
415
669
  const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
@@ -420,28 +674,61 @@ function normaliseSwaggerOperation(operation, doc) {
420
674
  if (Array.isArray(params)) {
421
675
  const nonBodyParams = [];
422
676
  let bodyParam;
677
+ let firstBodyName;
423
678
  let usesFormData = false;
424
- for (const param of params) {
679
+ for (const [index, param] of params.entries()) {
425
680
  if (!isObject(param)) {
426
681
  nonBodyParams.push(param);
427
682
  continue;
428
683
  }
429
684
  const resolvedParam = resolveSwaggerParameter(param, doc);
430
685
  const location = resolvedParam.in;
431
- if (location === "body") bodyParam = resolvedParam;
432
- else if (location === "formData") {
433
- bodyParam = buildFormDataBody(resolvedParam, params);
434
- usesFormData = true;
686
+ if (location === "body") {
687
+ if (bodyParam !== void 0) {
688
+ const duplicateName = typeof resolvedParam.name === "string" ? resolvedParam.name : `parameters[${String(index)}]`;
689
+ emitDiagnostic(diagnostics, {
690
+ code: "duplicate-body-parameter",
691
+ message: `Operation defines more than one "in: body" parameter; keeping the first ("${firstBodyName ?? "(unnamed)"}") and discarding "${duplicateName}"`,
692
+ pointer: appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"),
693
+ detail: {
694
+ kept: firstBodyName,
695
+ discarded: duplicateName,
696
+ location: "operation"
697
+ }
698
+ });
699
+ continue;
700
+ }
701
+ bodyParam = resolvedParam;
702
+ firstBodyName = typeof resolvedParam.name === "string" ? resolvedParam.name : void 0;
703
+ } else if (location === "formData") {
704
+ if (!usesFormData) {
705
+ bodyParam = buildFormDataBody(resolvedParam, params);
706
+ usesFormData = true;
707
+ }
435
708
  } else nonBodyParams.push(normaliseSwaggerParameter(resolvedParam, doc));
436
709
  }
437
710
  if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
438
- if (bodyParam !== void 0) result.requestBody = buildRequestBody(bodyParam, usesFormData ? ["multipart/form-data"] : consumes);
711
+ if (bodyParam !== void 0) result.requestBody = buildRequestBody(bodyParam, usesFormData ? formDataContentTypes(consumes) : consumes);
439
712
  }
440
713
  const responses = operation.responses;
441
714
  if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces);
442
715
  return result;
443
716
  }
444
717
  /**
718
+ * Determine the request body media type for a Swagger 2.0 formData operation.
719
+ *
720
+ * Per the OAS 3 conversion rules, `application/x-www-form-urlencoded` is
721
+ * preferred when the operation- or document-level `consumes` includes it;
722
+ * otherwise `multipart/form-data` is the default. File uploads (Swagger 2.0
723
+ * `type: file`) still require `multipart/form-data`, but the formData body
724
+ * schema-builder normalises them to `string` + `format: binary` either way
725
+ * and the choice of media type is left to the source document.
726
+ */
727
+ function formDataContentTypes(consumes) {
728
+ if (consumes.includes("application/x-www-form-urlencoded")) return ["application/x-www-form-urlencoded"];
729
+ return ["multipart/form-data"];
730
+ }
731
+ /**
445
732
  * Resolve a Swagger parameter that may be a `$ref`.
446
733
  */
447
734
  function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()) {
@@ -596,7 +883,7 @@ function normaliseSwaggerResponses(responses, doc, produces) {
596
883
  function normaliseSwaggerSingleResponse(response, doc, produces) {
597
884
  const resolved = resolveSwaggerResponse(response, doc);
598
885
  const normalised = {};
599
- for (const [key, value] of Object.entries(resolved)) if (key !== "schema") normalised[key] = value;
886
+ for (const [key, value] of Object.entries(resolved)) if (key !== "schema" && key !== "headers") normalised[key] = value;
600
887
  const schema = resolved.schema;
601
888
  if (isObject(schema)) {
602
889
  const content = {};
@@ -604,9 +891,64 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
604
891
  for (const ct of contentTypes) if (typeof ct === "string") content[ct] = { schema };
605
892
  normalised.content = content;
606
893
  }
894
+ const headers = resolved.headers;
895
+ if (isObject(headers)) {
896
+ const convertedHeaders = {};
897
+ for (const [name, header] of Object.entries(headers)) convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header) : header;
898
+ normalised.headers = convertedHeaders;
899
+ }
607
900
  return normalised;
608
901
  }
609
902
  /**
903
+ * Normalise a single Swagger 2.0 response header to OpenAPI 3.x form.
904
+ *
905
+ * Swagger 2.0 headers mirror parameter shape: `type`/`format`/
906
+ * `collectionFormat` live at the root. OpenAPI 3.x requires the type
907
+ * descriptor under `schema`, with collection serialisation expressed via
908
+ * `style`/`explode`. Headers do not carry `name` or `in` — those are not
909
+ * part of either spec at this level — so this is a thin sibling to
910
+ * `normaliseSwaggerParameter` rather than a full reuse. The OpenAPI 3.x
911
+ * default header style is `simple`, so CSV-encoded headers map to
912
+ * `simple`/`explode: false` rather than the `form` style used for query
913
+ * parameters.
914
+ */
915
+ function normaliseSwaggerHeader(header) {
916
+ const result = {};
917
+ for (const [key, value] of Object.entries(header)) {
918
+ if (key === "type" || key === "format" || key === "collectionFormat") continue;
919
+ result[key] = value;
920
+ }
921
+ if (typeof header.type === "string") {
922
+ const schema = { type: header.type };
923
+ if (typeof header.format === "string") schema.format = header.format;
924
+ if (header.enum !== void 0) schema.enum = header.enum;
925
+ if (header.default !== void 0) schema.default = header.default;
926
+ if (header.minimum !== void 0) schema.minimum = header.minimum;
927
+ if (header.maximum !== void 0) schema.maximum = header.maximum;
928
+ result.schema = schema;
929
+ }
930
+ const cf = header.collectionFormat;
931
+ if (typeof cf === "string") switch (cf) {
932
+ case "csv":
933
+ result.style = "simple";
934
+ result.explode = false;
935
+ break;
936
+ case "ssv":
937
+ result.style = "spaceDelimited";
938
+ result.explode = false;
939
+ break;
940
+ case "tsv":
941
+ result.style = "tabDelimited";
942
+ result.explode = false;
943
+ break;
944
+ case "pipes":
945
+ result.style = "pipeDelimited";
946
+ result.explode = false;
947
+ break;
948
+ }
949
+ return result;
950
+ }
951
+ /**
610
952
  * Mapping of Swagger 2.0 $ref prefixes to OpenAPI 3.x equivalents.
611
953
  * Applied after document restructuring so all $ref strings point
612
954
  * to the correct locations in the normalised document.
@@ -691,35 +1033,43 @@ const SINGLE_SUBSCHEMA_KEYS = new Set([
691
1033
  * Normalise each element of an unknown array by applying deepNormalise
692
1034
  * to object elements and passing others through unchanged.
693
1035
  */
694
- function normaliseArray(items, transform) {
1036
+ function normaliseArray(items, transform, visited) {
695
1037
  const result = [];
696
- for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform) : item);
1038
+ for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform, visited) : item);
697
1039
  return result;
698
1040
  }
699
1041
  /**
700
1042
  * Normalise each value of a sub-schema map (e.g. properties, $defs).
701
1043
  */
702
- function normaliseSubSchemaMap(map, transform) {
1044
+ function normaliseSubSchemaMap(map, transform, visited) {
703
1045
  const result = {};
704
- for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform) : v;
1046
+ for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform, visited) : v;
705
1047
  return result;
706
1048
  }
707
1049
  /**
708
1050
  * Deep-normalise a JSON Schema object by applying a per-node transform
709
1051
  * and recursing into every sub-schema location.
1052
+ *
1053
+ * The optional `visited` set guards against shared object references and
1054
+ * cycles introduced upstream (e.g. by the OpenAPI bundler's
1055
+ * `structuredClone`-based inlining of external refs). The walk skips
1056
+ * already-seen nodes by returning the original reference rather than
1057
+ * recursing forever.
710
1058
  */
711
- function deepNormalise(schema, transform) {
1059
+ function deepNormalise(schema, transform, visited = /* @__PURE__ */ new WeakSet()) {
1060
+ if (visited.has(schema)) return schema;
1061
+ visited.add(schema);
712
1062
  const node = transform({ ...schema });
713
1063
  const result = {};
714
- for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform);
715
- else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform);
716
- else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform);
717
- else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform);
718
- else if (isObject(value)) result[key] = deepNormalise(value, transform);
1064
+ for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform, visited);
1065
+ else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform, visited);
1066
+ else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform, visited);
1067
+ else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform, visited);
1068
+ else if (isObject(value)) result[key] = deepNormalise(value, transform, visited);
719
1069
  else result[key] = value;
720
1070
  else if (key === "dependencies" && isObject(value)) {
721
1071
  const normalised = {};
722
- for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform);
1072
+ for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform, visited);
723
1073
  else normalised[dk] = dv;
724
1074
  result[key] = normalised;
725
1075
  } else result[key] = value;
@@ -888,6 +1238,26 @@ function validateDependentRequired(node, ctx) {
888
1238
  }
889
1239
  }
890
1240
  /**
1241
+ * Translate the legacy tuple-form `items: [Schema, Schema]` keyword to
1242
+ * Draft 2020-12's `prefixItems`. `additionalItems` becomes the rest-element
1243
+ * schema and is rewritten to `items`.
1244
+ *
1245
+ * Applies to Drafts 04, 06, and 07 — all of which used the tuple form
1246
+ * before Draft 2020-12 split positional schemas into `prefixItems`.
1247
+ *
1248
+ * No-op when `items` is already a single sub-schema, or when `prefixItems`
1249
+ * is already present (do not overwrite an explicit author choice).
1250
+ */
1251
+ function translateTupleItems(node) {
1252
+ if (!Array.isArray(node.items) || "prefixItems" in node) return;
1253
+ node.prefixItems = node.items;
1254
+ delete node.items;
1255
+ if ("additionalItems" in node) {
1256
+ node.items = node.additionalItems;
1257
+ delete node.additionalItems;
1258
+ }
1259
+ }
1260
+ /**
891
1261
  * Apply the version-agnostic Draft 04 keyword translations to a single
892
1262
  * node: boolean exclusive-min/max → number form, bare `id` → `$id`, and
893
1263
  * tuple-form `items` → `prefixItems`.
@@ -906,10 +1276,28 @@ function applyDraft04Translations(node, ctx) {
906
1276
  node.exclusiveMinimum = node.minimum;
907
1277
  delete node.minimum;
908
1278
  } else if (node.exclusiveMinimum === false) delete node.exclusiveMinimum;
1279
+ else if (node.exclusiveMinimum === true) {
1280
+ if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1281
+ code: "bare-exclusive-bound",
1282
+ message: "`exclusiveMinimum: true` requires a sibling numeric `minimum` in Draft 04; dropping the keyword",
1283
+ pointer: appendPointer(ctx.pointer, "exclusiveMinimum"),
1284
+ detail: { keyword: "exclusiveMinimum" }
1285
+ });
1286
+ delete node.exclusiveMinimum;
1287
+ }
909
1288
  if (node.exclusiveMaximum === true && typeof node.maximum === "number") {
910
1289
  node.exclusiveMaximum = node.maximum;
911
1290
  delete node.maximum;
912
1291
  } else if (node.exclusiveMaximum === false) delete node.exclusiveMaximum;
1292
+ else if (node.exclusiveMaximum === true) {
1293
+ if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1294
+ code: "bare-exclusive-bound",
1295
+ message: "`exclusiveMaximum: true` requires a sibling numeric `maximum` in Draft 04; dropping the keyword",
1296
+ pointer: appendPointer(ctx.pointer, "exclusiveMaximum"),
1297
+ detail: { keyword: "exclusiveMaximum" }
1298
+ });
1299
+ delete node.exclusiveMaximum;
1300
+ }
913
1301
  const divisibleBy = node.divisibleBy;
914
1302
  if (typeof divisibleBy === "number") {
915
1303
  const multipleOf = node.multipleOf;
@@ -930,14 +1318,7 @@ function applyDraft04Translations(node, ctx) {
930
1318
  node.$id = node.id;
931
1319
  delete node.id;
932
1320
  }
933
- if (Array.isArray(node.items) && !("prefixItems" in node)) {
934
- node.prefixItems = node.items;
935
- delete node.items;
936
- if ("additionalItems" in node) {
937
- node.items = node.additionalItems;
938
- delete node.additionalItems;
939
- }
940
- }
1321
+ translateTupleItems(node);
941
1322
  splitDependencies(node, ctx, false);
942
1323
  validateDependentRequired(node, ctx);
943
1324
  }
@@ -983,8 +1364,14 @@ function normaliseDraft04NodeWithContext(node, ctx) {
983
1364
  * (already in final form) and `const`/`examples`, but still use the
984
1365
  * legacy `dependencies` keyword. Split it into `dependentRequired` /
985
1366
  * `dependentSchemas` so the walker can process them uniformly.
1367
+ *
1368
+ * Drafts 06 and 07 also use the tuple form
1369
+ * `items: [Schema, Schema], additionalItems: Schema` — translate that to
1370
+ * Draft 2020-12's `prefixItems` + rest-`items` so the walker produces a
1371
+ * `TupleField` rather than silently dropping the positional schemas.
986
1372
  */
987
1373
  function normaliseDraft06Or07NodeWithContext(node, ctx) {
1374
+ translateTupleItems(node);
988
1375
  splitDependencies(node, ctx, false);
989
1376
  validateDependentRequired(node, ctx);
990
1377
  return node;
@@ -1045,15 +1432,153 @@ function normaliseJsonSchema(schema, draft, diagnostics) {
1045
1432
  diagnostics,
1046
1433
  pointer: ""
1047
1434
  };
1435
+ let normalised;
1048
1436
  switch (draft) {
1049
- case "draft-04": return deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
1050
- case "draft-2019-09": return deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
1051
- case "draft-2020-12": return deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
1437
+ case "draft-04":
1438
+ normalised = deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
1439
+ break;
1440
+ case "draft-2019-09":
1441
+ normalised = deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
1442
+ break;
1443
+ case "draft-2020-12":
1444
+ normalised = deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
1445
+ break;
1052
1446
  case "draft-06":
1053
- case "draft-07": return deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
1447
+ case "draft-07":
1448
+ normalised = deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
1449
+ break;
1450
+ }
1451
+ return resolveRelativeRefs(normalised, diagnostics);
1452
+ }
1453
+ /**
1454
+ * Parse a string as an absolute URI, returning `undefined` when it has
1455
+ * no scheme. Used to detect whether an `$id` value defines a base URI.
1456
+ */
1457
+ function parseAbsoluteUri(value) {
1458
+ if (typeof value !== "string" || value.length === 0) return void 0;
1459
+ try {
1460
+ const url = new URL(value);
1461
+ if (url.protocol.length === 0) return void 0;
1462
+ return url;
1463
+ } catch {
1464
+ return;
1054
1465
  }
1055
1466
  }
1056
1467
  /**
1468
+ * Resolve a relative reference against a base URI. Returns `undefined`
1469
+ * when the reference cannot be resolved (e.g. malformed input).
1470
+ */
1471
+ function resolveAgainst(ref, base) {
1472
+ try {
1473
+ return new URL(ref, base);
1474
+ } catch {
1475
+ return;
1476
+ }
1477
+ }
1478
+ /**
1479
+ * Strip the fragment portion from a URL, returning the canonical
1480
+ * `scheme://authority/path?query` form. Used to compare a resolved
1481
+ * `$ref` URI against the document's `$id` base.
1482
+ */
1483
+ function stripFragment(url) {
1484
+ const clone = new URL(url.toString());
1485
+ clone.hash = "";
1486
+ return clone.toString();
1487
+ }
1488
+ /**
1489
+ * Recursively rewrite relative `$ref`s in a schema so they resolve
1490
+ * correctly under the JSON Schema base-URI rules (RFC 3986 + JSON
1491
+ * Schema §8.2). Refs that resolve to the document's own `$id` are
1492
+ * rewritten to fragment-only form so the existing dereferencer can
1493
+ * handle them; refs that resolve outside the document are left as
1494
+ * absolute URIs (handled by the external resolver path).
1495
+ *
1496
+ * Returns the input unchanged when the document has no base URI or
1497
+ * no relative refs.
1498
+ */
1499
+ function resolveRelativeRefs(schema, diagnostics) {
1500
+ const docBaseUrl = parseAbsoluteUri(schema.$id);
1501
+ if (docBaseUrl === void 0) return schema;
1502
+ const docBase = stripFragment(docBaseUrl);
1503
+ return rewriteRelativeRefsNode(schema, docBase, docBase, "", diagnostics, /* @__PURE__ */ new WeakSet());
1504
+ }
1505
+ /**
1506
+ * Rewrite relative `$ref`s in a single node, recursing into sub-schemas.
1507
+ *
1508
+ * The `visited` set guards against shared object references and cycles
1509
+ * introduced upstream (e.g. by the OpenAPI bundler's `structuredClone`
1510
+ * inlining of external refs). When a node is re-encountered the rewrite
1511
+ * is short-circuited — returning the original reference unchanged is
1512
+ * sound because every relative `$ref` in the subtree was already
1513
+ * rewritten on the first visit.
1514
+ */
1515
+ function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostics, visited) {
1516
+ if (visited.has(node)) return node;
1517
+ visited.add(node);
1518
+ let nextBase = currentBase;
1519
+ const nodeId = node.$id;
1520
+ if (typeof nodeId === "string" && nodeId.length > 0) {
1521
+ const resolved = resolveAgainst(nodeId, currentBase);
1522
+ if (resolved !== void 0) nextBase = stripFragment(resolved);
1523
+ }
1524
+ const result = {};
1525
+ for (const [key, value] of Object.entries(node)) {
1526
+ if (key === "$ref" && typeof value === "string") {
1527
+ result[key] = rewriteRef(value, nextBase, docBase, appendPointer(pointer, key), diagnostics);
1528
+ continue;
1529
+ }
1530
+ result[key] = rewriteRelativeRefsValue(value, nextBase, docBase, appendPointer(pointer, key), diagnostics, visited);
1531
+ }
1532
+ return result;
1533
+ }
1534
+ function rewriteRelativeRefsValue(value, currentBase, docBase, pointer, diagnostics, visited) {
1535
+ if (Array.isArray(value)) {
1536
+ if (visited.has(value)) return value;
1537
+ visited.add(value);
1538
+ return value.map((item, i) => rewriteRelativeRefsValue(item, currentBase, docBase, appendPointer(pointer, String(i)), diagnostics, visited));
1539
+ }
1540
+ if (isObject(value)) return rewriteRelativeRefsNode(value, currentBase, docBase, pointer, diagnostics, visited);
1541
+ return value;
1542
+ }
1543
+ /**
1544
+ * Rewrite a single `$ref` string. Fragment-only refs and refs that
1545
+ * already include a scheme are returned unchanged. Relative refs are
1546
+ * resolved against `currentBase`; if the result lives in the same
1547
+ * document as `docBase`, the ref is rewritten to fragment form.
1548
+ */
1549
+ function rewriteRef(ref, currentBase, docBase, pointer, diagnostics) {
1550
+ if (ref.startsWith("#")) return ref;
1551
+ if (/^[a-z][a-z0-9+\-.]*:/i.test(ref)) return ref;
1552
+ const resolved = resolveAgainst(ref, currentBase);
1553
+ if (resolved === void 0) return ref;
1554
+ if (stripFragment(resolved) === docBase) {
1555
+ const fragment = resolved.hash === "" ? "#" : resolved.hash;
1556
+ emitDiagnostic(diagnostics, {
1557
+ code: "relative-ref-resolved",
1558
+ message: `Relative $ref "${ref}" resolved to "${fragment}" against base "${currentBase}"`,
1559
+ pointer,
1560
+ detail: {
1561
+ ref,
1562
+ base: currentBase,
1563
+ resolved: fragment
1564
+ }
1565
+ });
1566
+ return fragment;
1567
+ }
1568
+ const absolute = resolved.toString();
1569
+ emitDiagnostic(diagnostics, {
1570
+ code: "relative-ref-resolved",
1571
+ message: `Relative $ref "${ref}" resolved to "${absolute}" against base "${currentBase}"`,
1572
+ pointer,
1573
+ detail: {
1574
+ ref,
1575
+ base: currentBase,
1576
+ resolved: absolute
1577
+ }
1578
+ });
1579
+ return absolute;
1580
+ }
1581
+ /**
1057
1582
  * Normalise an OpenAPI document's schemas for walker consumption.
1058
1583
  * Handles version-specific keyword transformations.
1059
1584
  *
@@ -1062,8 +1587,17 @@ function normaliseJsonSchema(schema, draft, diagnostics) {
1062
1587
  */
1063
1588
  function normaliseOpenApiSchemas(doc, version, diagnostics) {
1064
1589
  if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics);
1065
- if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(doc, deepNormalise);
1066
- return deepNormaliseOpenApiDoc(doc, (schema) => deepNormalise(schema, normaliseOpenApi30Discriminator));
1590
+ if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(applyDiscriminatorAllOfPrepass(doc), deepNormalise);
1591
+ if (isOpenApi31(version)) {
1592
+ const dialect = readJsonSchemaDialect(doc);
1593
+ if (dialect.kind === "unknown") emitDiagnostic(diagnostics, {
1594
+ code: "unknown-json-schema-dialect",
1595
+ message: `OpenAPI 3.1 \`jsonSchemaDialect\` URI "${dialect.uri}" does not match a supported JSON Schema draft; falling back to Draft 2020-12`,
1596
+ pointer: "/jsonSchemaDialect",
1597
+ detail: { uri: dialect.uri }
1598
+ });
1599
+ }
1600
+ return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => resolveRelativeRefs(deepNormalise(schema, normaliseOpenApi30Discriminator), diagnostics));
1067
1601
  }
1068
1602
  //#endregion
1069
- export { normaliseOpenApiSchemas as a, deepNormaliseOpenApiDoc as c, normaliseOpenApi30Node as d, normaliseJsonSchema as i, normaliseOpenApi30Combined as l, deepNormaliseWithContext as n, normaliseSwagger2Document as o, normaliseDraft04Node as r, deepNormaliseOpenApi30Doc as s, deepNormalise as t, normaliseOpenApi30Discriminator as u };
1603
+ export { normaliseOpenApiSchemas as a, deepNormaliseOpenApi30Doc as c, normaliseOpenApi30Discriminator as d, normaliseOpenApi30Node as f, normaliseJsonSchema as i, deepNormaliseOpenApiDoc as l, deepNormaliseWithContext as n, normaliseSwagger2Document as o, normaliseDraft04Node as r, applyDiscriminatorAllOfPrepass as s, deepNormalise as t, normaliseOpenApi30Combined as u };