@skmtc/core 0.32.0 → 0.32.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.
@@ -171,16 +171,79 @@ const normalizeContentFormat = (schema) => {
171
171
  const { contentMediaType: _contentMediaType, contentEncoding: _contentEncoding, ...rest } = view;
172
172
  return 'format' in rest ? rest : { ...rest, format: derivedFormat };
173
173
  };
174
+ /**
175
+ * In-place applicators that evaluate keys `unevaluatedProperties` would
176
+ * otherwise see. `$ref` and `not` never reach `normalizeSchemaShape`.
177
+ */
178
+ const inPlaceApplicators = [
179
+ 'allOf',
180
+ 'oneOf',
181
+ 'anyOf',
182
+ 'if',
183
+ 'then',
184
+ 'else',
185
+ 'dependentSchemas',
186
+ '$dynamicRef'
187
+ ];
188
+ /**
189
+ * With no in-place applicator on the same schema, `unevaluatedProperties`
190
+ * means the same as `additionalProperties` (JSON Schema 2020-12 §11.3,
191
+ * §10.3.2.3), so rename it. When `additionalProperties` is also present it
192
+ * evaluates every remaining key, so `unevaluatedProperties` is dropped.
193
+ */
194
+ const normalizeUnevaluatedProperties = (schema) => {
195
+ if (!('unevaluatedProperties' in schema)) {
196
+ return schema;
197
+ }
198
+ if (inPlaceApplicators.some(keyword => keyword in schema)) {
199
+ return schema;
200
+ }
201
+ // `unevaluatedProperties` is a 3.1 keyword the 3.0-typed SchemaObject does
202
+ // not model; read it via an annotated destructure.
203
+ const { unevaluatedProperties, ...rest } = schema;
204
+ if ('additionalProperties' in rest) {
205
+ return rest;
206
+ }
207
+ if (!isAdditionalPropertiesValue(unevaluatedProperties)) {
208
+ return schema;
209
+ }
210
+ return { ...rest, additionalProperties: unevaluatedProperties };
211
+ };
212
+ /**
213
+ * Is this a value `additionalProperties` accepts: a boolean, or a schema or
214
+ * `$ref` object?
215
+ */
216
+ const isAdditionalPropertiesValue = (value) => typeof value === 'boolean' ||
217
+ (typeof value === 'object' && value !== null && !Array.isArray(value));
218
+ /**
219
+ * Remove `unevaluatedProperties` from an `allOf`/`oneOf`/`anyOf` wrapper and
220
+ * log it as skipped. Left in place, it would be renamed to
221
+ * `additionalProperties` when the merged or collapsed schema is dispatched
222
+ * again with no composition keyword left.
223
+ */
224
+ const withoutWrapperUnevaluatedProperties = ({ schema, parentType, stackTrail, context }) => {
225
+ if (!('unevaluatedProperties' in schema)) {
226
+ return schema;
227
+ }
228
+ const { unevaluatedProperties, ...rest } = schema;
229
+ context.logSkippedFields({
230
+ skipped: { unevaluatedProperties },
231
+ parent: schema,
232
+ parentType,
233
+ stackTrail
234
+ });
235
+ return rest;
236
+ };
174
237
  /**
175
238
  * Map the 3.1-only schema-shape encodings onto the shared IR shape before
176
- * dispatching: type arrays, `const`, `examples[]`, numeric exclusive bounds,
177
- * and binary/base64 content keywords. Each step returns its input unchanged
178
- * when it does not apply, so the composed result is referentially equal to
179
- * `schema` when nothing matched — letting the dispatcher re-dispatch only when
180
- * needed.
239
+ * dispatching: `unevaluatedProperties`, type arrays, `const`, `examples[]`,
240
+ * numeric exclusive bounds, and binary/base64 content keywords. Each step
241
+ * returns its input unchanged when it does not apply, so the composed result
242
+ * is referentially equal to `schema` when nothing matched — letting the
243
+ * dispatcher re-dispatch only when needed.
181
244
  */
182
245
  const normalizeSchemaShape = (schema) => {
183
- return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(schema)))));
246
+ return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(normalizeUnevaluatedProperties(schema))))));
184
247
  };
185
248
  /**
186
249
  * Is this a 3.1 `{ type: 'null' }` schema member? (`'null'` is a 3.1 type
@@ -252,6 +315,12 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
252
315
  return toSchemaV3({ schema: normalized, stackTrail, context });
253
316
  }
254
317
  if ('allOf' in schema && Array.isArray(schema.allOf)) {
318
+ const wrapper = withoutWrapperUnevaluatedProperties({
319
+ schema,
320
+ parentType: 'allOf',
321
+ stackTrail,
322
+ context
323
+ });
255
324
  return stackTrail.trace('allOf', st => {
256
325
  // A single-member allOf collapses to its sole member — the value must
257
326
  // satisfy exactly that one schema. When the member is a `$ref`, keep it
@@ -261,7 +330,7 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
261
330
  // self-referential, and eagerly resolving it would not terminate. The
262
331
  // ref resolves at use time like every other ref. Mirrors the
263
332
  // single-member oneOf/anyOf handling below.
264
- const { allOf, ...value } = schema;
333
+ const { allOf, ...value } = wrapper;
265
334
  const members = allOf ?? [];
266
335
  if (members.length === 1) {
267
336
  const [member] = members;
@@ -281,20 +350,26 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
281
350
  });
282
351
  }
283
352
  const merged = mergeIntersection({
284
- schema,
353
+ schema: wrapper,
285
354
  getRef: toGetRef(context.documentObject)
286
355
  });
287
356
  return toSchemaV3({ schema: merged, stackTrail: st, context });
288
357
  });
289
358
  }
290
359
  if ('oneOf' in schema && Array.isArray(schema.oneOf)) {
360
+ const wrapper = withoutWrapperUnevaluatedProperties({
361
+ schema,
362
+ parentType: 'oneOf',
363
+ stackTrail,
364
+ context
365
+ });
291
366
  return stackTrail.trace('oneOf', st => {
292
367
  // 3.1: a `{ type: 'null' }` member makes the union nullable. Fold it
293
368
  // into a `nullable` flag the collapse/union logic already understands.
294
- const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.oneOf ?? []);
369
+ const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.oneOf ?? []);
295
370
  const withoutNull = hasNullMember
296
- ? { ...schema, oneOf: oneOfWithoutNull, nullable: true }
297
- : schema;
371
+ ? { ...wrapper, oneOf: oneOfWithoutNull, nullable: true }
372
+ : wrapper;
298
373
  const merged = mergeUnion({
299
374
  schema: withoutNull,
300
375
  getRef: toGetRef(context.documentObject),
@@ -337,19 +412,25 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
337
412
  });
338
413
  }
339
414
  if ('anyOf' in schema && Array.isArray(schema.anyOf)) {
415
+ const wrapper = withoutWrapperUnevaluatedProperties({
416
+ schema,
417
+ parentType: 'anyOf',
418
+ stackTrail,
419
+ context
420
+ });
340
421
  return stackTrail.trace('anyOf', st => {
341
422
  // deno-lint-ignore ban-ts-comment
342
423
  // @ts-expect-error
343
- if (schema['x-expansionResources'] && Array.isArray(schema.anyOf)) {
344
- const { anyOf, ...value } = schema;
424
+ if (wrapper['x-expansionResources'] && Array.isArray(wrapper.anyOf)) {
425
+ const { anyOf, ...value } = wrapper;
345
426
  return toUnion({ value, members: anyOf, parentType: 'anyOf', stackTrail: st, context });
346
427
  }
347
428
  // 3.1: a `{ type: 'null' }` member makes the union nullable (mirrors
348
429
  // the oneOf branch above).
349
- const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.anyOf ?? []);
430
+ const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.anyOf ?? []);
350
431
  const withoutNull = hasNullMember
351
- ? { ...schema, anyOf: anyOfWithoutNull, nullable: true }
352
- : schema;
432
+ ? { ...wrapper, anyOf: anyOfWithoutNull, nullable: true }
433
+ : wrapper;
353
434
  const merged = mergeUnion({
354
435
  schema: withoutNull,
355
436
  getRef: toGetRef(context.documentObject),
@@ -171,16 +171,79 @@ const normalizeContentFormat = (schema) => {
171
171
  const { contentMediaType: _contentMediaType, contentEncoding: _contentEncoding, ...rest } = view;
172
172
  return 'format' in rest ? rest : { ...rest, format: derivedFormat };
173
173
  };
174
+ /**
175
+ * In-place applicators that evaluate keys `unevaluatedProperties` would
176
+ * otherwise see. `$ref` and `not` never reach `normalizeSchemaShape`.
177
+ */
178
+ const inPlaceApplicators = [
179
+ 'allOf',
180
+ 'oneOf',
181
+ 'anyOf',
182
+ 'if',
183
+ 'then',
184
+ 'else',
185
+ 'dependentSchemas',
186
+ '$dynamicRef'
187
+ ];
188
+ /**
189
+ * With no in-place applicator on the same schema, `unevaluatedProperties`
190
+ * means the same as `additionalProperties` (JSON Schema 2020-12 §11.3,
191
+ * §10.3.2.3), so rename it. When `additionalProperties` is also present it
192
+ * evaluates every remaining key, so `unevaluatedProperties` is dropped.
193
+ */
194
+ const normalizeUnevaluatedProperties = (schema) => {
195
+ if (!('unevaluatedProperties' in schema)) {
196
+ return schema;
197
+ }
198
+ if (inPlaceApplicators.some(keyword => keyword in schema)) {
199
+ return schema;
200
+ }
201
+ // `unevaluatedProperties` is a 3.1 keyword the 3.0-typed SchemaObject does
202
+ // not model; read it via an annotated destructure.
203
+ const { unevaluatedProperties, ...rest } = schema;
204
+ if ('additionalProperties' in rest) {
205
+ return rest;
206
+ }
207
+ if (!isAdditionalPropertiesValue(unevaluatedProperties)) {
208
+ return schema;
209
+ }
210
+ return { ...rest, additionalProperties: unevaluatedProperties };
211
+ };
212
+ /**
213
+ * Is this a value `additionalProperties` accepts: a boolean, or a schema or
214
+ * `$ref` object?
215
+ */
216
+ const isAdditionalPropertiesValue = (value) => typeof value === 'boolean' ||
217
+ (typeof value === 'object' && value !== null && !Array.isArray(value));
218
+ /**
219
+ * Remove `unevaluatedProperties` from an `allOf`/`oneOf`/`anyOf` wrapper and
220
+ * log it as skipped. Left in place, it would be renamed to
221
+ * `additionalProperties` when the merged or collapsed schema is dispatched
222
+ * again with no composition keyword left.
223
+ */
224
+ const withoutWrapperUnevaluatedProperties = ({ schema, parentType, stackTrail, context }) => {
225
+ if (!('unevaluatedProperties' in schema)) {
226
+ return schema;
227
+ }
228
+ const { unevaluatedProperties, ...rest } = schema;
229
+ context.logSkippedFields({
230
+ skipped: { unevaluatedProperties },
231
+ parent: schema,
232
+ parentType,
233
+ stackTrail
234
+ });
235
+ return rest;
236
+ };
174
237
  /**
175
238
  * Map the 3.1-only schema-shape encodings onto the shared IR shape before
176
- * dispatching: type arrays, `const`, `examples[]`, numeric exclusive bounds,
177
- * and binary/base64 content keywords. Each step returns its input unchanged
178
- * when it does not apply, so the composed result is referentially equal to
179
- * `schema` when nothing matched — letting the dispatcher re-dispatch only when
180
- * needed.
239
+ * dispatching: `unevaluatedProperties`, type arrays, `const`, `examples[]`,
240
+ * numeric exclusive bounds, and binary/base64 content keywords. Each step
241
+ * returns its input unchanged when it does not apply, so the composed result
242
+ * is referentially equal to `schema` when nothing matched — letting the
243
+ * dispatcher re-dispatch only when needed.
181
244
  */
182
245
  const normalizeSchemaShape = (schema) => {
183
- return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(schema)))));
246
+ return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(normalizeUnevaluatedProperties(schema))))));
184
247
  };
185
248
  /**
186
249
  * Is this a 3.1 `{ type: 'null' }` schema member? (`'null'` is a 3.1 type
@@ -252,6 +315,12 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
252
315
  return toSchemaV3({ schema: normalized, stackTrail, context });
253
316
  }
254
317
  if ('allOf' in schema && Array.isArray(schema.allOf)) {
318
+ const wrapper = withoutWrapperUnevaluatedProperties({
319
+ schema,
320
+ parentType: 'allOf',
321
+ stackTrail,
322
+ context
323
+ });
255
324
  return stackTrail.trace('allOf', st => {
256
325
  // A single-member allOf collapses to its sole member — the value must
257
326
  // satisfy exactly that one schema. When the member is a `$ref`, keep it
@@ -261,7 +330,7 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
261
330
  // self-referential, and eagerly resolving it would not terminate. The
262
331
  // ref resolves at use time like every other ref. Mirrors the
263
332
  // single-member oneOf/anyOf handling below.
264
- const { allOf, ...value } = schema;
333
+ const { allOf, ...value } = wrapper;
265
334
  const members = allOf ?? [];
266
335
  if (members.length === 1) {
267
336
  const [member] = members;
@@ -281,20 +350,26 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
281
350
  });
282
351
  }
283
352
  const merged = mergeIntersection({
284
- schema,
353
+ schema: wrapper,
285
354
  getRef: toGetRef(context.documentObject)
286
355
  });
287
356
  return toSchemaV3({ schema: merged, stackTrail: st, context });
288
357
  });
289
358
  }
290
359
  if ('oneOf' in schema && Array.isArray(schema.oneOf)) {
360
+ const wrapper = withoutWrapperUnevaluatedProperties({
361
+ schema,
362
+ parentType: 'oneOf',
363
+ stackTrail,
364
+ context
365
+ });
291
366
  return stackTrail.trace('oneOf', st => {
292
367
  // 3.1: a `{ type: 'null' }` member makes the union nullable. Fold it
293
368
  // into a `nullable` flag the collapse/union logic already understands.
294
- const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.oneOf ?? []);
369
+ const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.oneOf ?? []);
295
370
  const withoutNull = hasNullMember
296
- ? { ...schema, oneOf: oneOfWithoutNull, nullable: true }
297
- : schema;
371
+ ? { ...wrapper, oneOf: oneOfWithoutNull, nullable: true }
372
+ : wrapper;
298
373
  const merged = mergeUnion({
299
374
  schema: withoutNull,
300
375
  getRef: toGetRef(context.documentObject),
@@ -337,19 +412,25 @@ export const toSchemaV3 = ({ schema, stackTrail, context }) => {
337
412
  });
338
413
  }
339
414
  if ('anyOf' in schema && Array.isArray(schema.anyOf)) {
415
+ const wrapper = withoutWrapperUnevaluatedProperties({
416
+ schema,
417
+ parentType: 'anyOf',
418
+ stackTrail,
419
+ context
420
+ });
340
421
  return stackTrail.trace('anyOf', st => {
341
422
  // deno-lint-ignore ban-ts-comment
342
423
  // @ts-expect-error
343
- if (schema['x-expansionResources'] && Array.isArray(schema.anyOf)) {
344
- const { anyOf, ...value } = schema;
424
+ if (wrapper['x-expansionResources'] && Array.isArray(wrapper.anyOf)) {
425
+ const { anyOf, ...value } = wrapper;
345
426
  return toUnion({ value, members: anyOf, parentType: 'anyOf', stackTrail: st, context });
346
427
  }
347
428
  // 3.1: a `{ type: 'null' }` member makes the union nullable (mirrors
348
429
  // the oneOf branch above).
349
- const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.anyOf ?? []);
430
+ const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.anyOf ?? []);
350
431
  const withoutNull = hasNullMember
351
- ? { ...schema, anyOf: anyOfWithoutNull, nullable: true }
352
- : schema;
432
+ ? { ...wrapper, anyOf: anyOfWithoutNull, nullable: true }
433
+ : wrapper;
353
434
  const merged = mergeUnion({
354
435
  schema: withoutNull,
355
436
  getRef: toGetRef(context.documentObject),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skmtc/core",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "Skmtc is a declarative code generation framework",
5
5
  "keywords": [
6
6
  "openapi",
@@ -176,16 +176,79 @@ const normalizeContentFormat = (schema) => {
176
176
  const { contentMediaType: _contentMediaType, contentEncoding: _contentEncoding, ...rest } = view;
177
177
  return 'format' in rest ? rest : { ...rest, format: derivedFormat };
178
178
  };
179
+ /**
180
+ * In-place applicators that evaluate keys `unevaluatedProperties` would
181
+ * otherwise see. `$ref` and `not` never reach `normalizeSchemaShape`.
182
+ */
183
+ const inPlaceApplicators = [
184
+ 'allOf',
185
+ 'oneOf',
186
+ 'anyOf',
187
+ 'if',
188
+ 'then',
189
+ 'else',
190
+ 'dependentSchemas',
191
+ '$dynamicRef'
192
+ ];
193
+ /**
194
+ * With no in-place applicator on the same schema, `unevaluatedProperties`
195
+ * means the same as `additionalProperties` (JSON Schema 2020-12 §11.3,
196
+ * §10.3.2.3), so rename it. When `additionalProperties` is also present it
197
+ * evaluates every remaining key, so `unevaluatedProperties` is dropped.
198
+ */
199
+ const normalizeUnevaluatedProperties = (schema) => {
200
+ if (!('unevaluatedProperties' in schema)) {
201
+ return schema;
202
+ }
203
+ if (inPlaceApplicators.some(keyword => keyword in schema)) {
204
+ return schema;
205
+ }
206
+ // `unevaluatedProperties` is a 3.1 keyword the 3.0-typed SchemaObject does
207
+ // not model; read it via an annotated destructure.
208
+ const { unevaluatedProperties, ...rest } = schema;
209
+ if ('additionalProperties' in rest) {
210
+ return rest;
211
+ }
212
+ if (!isAdditionalPropertiesValue(unevaluatedProperties)) {
213
+ return schema;
214
+ }
215
+ return { ...rest, additionalProperties: unevaluatedProperties };
216
+ };
217
+ /**
218
+ * Is this a value `additionalProperties` accepts: a boolean, or a schema or
219
+ * `$ref` object?
220
+ */
221
+ const isAdditionalPropertiesValue = (value) => typeof value === 'boolean' ||
222
+ (typeof value === 'object' && value !== null && !Array.isArray(value));
223
+ /**
224
+ * Remove `unevaluatedProperties` from an `allOf`/`oneOf`/`anyOf` wrapper and
225
+ * log it as skipped. Left in place, it would be renamed to
226
+ * `additionalProperties` when the merged or collapsed schema is dispatched
227
+ * again with no composition keyword left.
228
+ */
229
+ const withoutWrapperUnevaluatedProperties = ({ schema, parentType, stackTrail, context }) => {
230
+ if (!('unevaluatedProperties' in schema)) {
231
+ return schema;
232
+ }
233
+ const { unevaluatedProperties, ...rest } = schema;
234
+ context.logSkippedFields({
235
+ skipped: { unevaluatedProperties },
236
+ parent: schema,
237
+ parentType,
238
+ stackTrail
239
+ });
240
+ return rest;
241
+ };
179
242
  /**
180
243
  * Map the 3.1-only schema-shape encodings onto the shared IR shape before
181
- * dispatching: type arrays, `const`, `examples[]`, numeric exclusive bounds,
182
- * and binary/base64 content keywords. Each step returns its input unchanged
183
- * when it does not apply, so the composed result is referentially equal to
184
- * `schema` when nothing matched — letting the dispatcher re-dispatch only when
185
- * needed.
244
+ * dispatching: `unevaluatedProperties`, type arrays, `const`, `examples[]`,
245
+ * numeric exclusive bounds, and binary/base64 content keywords. Each step
246
+ * returns its input unchanged when it does not apply, so the composed result
247
+ * is referentially equal to `schema` when nothing matched — letting the
248
+ * dispatcher re-dispatch only when needed.
186
249
  */
187
250
  const normalizeSchemaShape = (schema) => {
188
- return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(schema)))));
251
+ return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(normalizeUnevaluatedProperties(schema))))));
189
252
  };
190
253
  /**
191
254
  * Is this a 3.1 `{ type: 'null' }` schema member? (`'null'` is a 3.1 type
@@ -257,6 +320,12 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
257
320
  return (0, exports.toSchemaV3)({ schema: normalized, stackTrail, context });
258
321
  }
259
322
  if ('allOf' in schema && Array.isArray(schema.allOf)) {
323
+ const wrapper = withoutWrapperUnevaluatedProperties({
324
+ schema,
325
+ parentType: 'allOf',
326
+ stackTrail,
327
+ context
328
+ });
260
329
  return stackTrail.trace('allOf', st => {
261
330
  // A single-member allOf collapses to its sole member — the value must
262
331
  // satisfy exactly that one schema. When the member is a `$ref`, keep it
@@ -266,7 +335,7 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
266
335
  // self-referential, and eagerly resolving it would not terminate. The
267
336
  // ref resolves at use time like every other ref. Mirrors the
268
337
  // single-member oneOf/anyOf handling below.
269
- const { allOf, ...value } = schema;
338
+ const { allOf, ...value } = wrapper;
270
339
  const members = allOf ?? [];
271
340
  if (members.length === 1) {
272
341
  const [member] = members;
@@ -286,20 +355,26 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
286
355
  });
287
356
  }
288
357
  const merged = (0, merge_intersection_js_1.mergeIntersection)({
289
- schema,
358
+ schema: wrapper,
290
359
  getRef: (0, refFns_js_2.toGetRef)(context.documentObject)
291
360
  });
292
361
  return (0, exports.toSchemaV3)({ schema: merged, stackTrail: st, context });
293
362
  });
294
363
  }
295
364
  if ('oneOf' in schema && Array.isArray(schema.oneOf)) {
365
+ const wrapper = withoutWrapperUnevaluatedProperties({
366
+ schema,
367
+ parentType: 'oneOf',
368
+ stackTrail,
369
+ context
370
+ });
296
371
  return stackTrail.trace('oneOf', st => {
297
372
  // 3.1: a `{ type: 'null' }` member makes the union nullable. Fold it
298
373
  // into a `nullable` flag the collapse/union logic already understands.
299
- const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.oneOf ?? []);
374
+ const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.oneOf ?? []);
300
375
  const withoutNull = hasNullMember
301
- ? { ...schema, oneOf: oneOfWithoutNull, nullable: true }
302
- : schema;
376
+ ? { ...wrapper, oneOf: oneOfWithoutNull, nullable: true }
377
+ : wrapper;
303
378
  const merged = (0, merge_union_js_1.mergeUnion)({
304
379
  schema: withoutNull,
305
380
  getRef: (0, refFns_js_2.toGetRef)(context.documentObject),
@@ -342,19 +417,25 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
342
417
  });
343
418
  }
344
419
  if ('anyOf' in schema && Array.isArray(schema.anyOf)) {
420
+ const wrapper = withoutWrapperUnevaluatedProperties({
421
+ schema,
422
+ parentType: 'anyOf',
423
+ stackTrail,
424
+ context
425
+ });
345
426
  return stackTrail.trace('anyOf', st => {
346
427
  // deno-lint-ignore ban-ts-comment
347
428
  // @ts-expect-error
348
- if (schema['x-expansionResources'] && Array.isArray(schema.anyOf)) {
349
- const { anyOf, ...value } = schema;
429
+ if (wrapper['x-expansionResources'] && Array.isArray(wrapper.anyOf)) {
430
+ const { anyOf, ...value } = wrapper;
350
431
  return (0, toUnion_js_1.toUnion)({ value, members: anyOf, parentType: 'anyOf', stackTrail: st, context });
351
432
  }
352
433
  // 3.1: a `{ type: 'null' }` member makes the union nullable (mirrors
353
434
  // the oneOf branch above).
354
- const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.anyOf ?? []);
435
+ const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.anyOf ?? []);
355
436
  const withoutNull = hasNullMember
356
- ? { ...schema, anyOf: anyOfWithoutNull, nullable: true }
357
- : schema;
437
+ ? { ...wrapper, anyOf: anyOfWithoutNull, nullable: true }
438
+ : wrapper;
358
439
  const merged = (0, merge_union_js_1.mergeUnion)({
359
440
  schema: withoutNull,
360
441
  getRef: (0, refFns_js_2.toGetRef)(context.documentObject),
@@ -176,16 +176,79 @@ const normalizeContentFormat = (schema) => {
176
176
  const { contentMediaType: _contentMediaType, contentEncoding: _contentEncoding, ...rest } = view;
177
177
  return 'format' in rest ? rest : { ...rest, format: derivedFormat };
178
178
  };
179
+ /**
180
+ * In-place applicators that evaluate keys `unevaluatedProperties` would
181
+ * otherwise see. `$ref` and `not` never reach `normalizeSchemaShape`.
182
+ */
183
+ const inPlaceApplicators = [
184
+ 'allOf',
185
+ 'oneOf',
186
+ 'anyOf',
187
+ 'if',
188
+ 'then',
189
+ 'else',
190
+ 'dependentSchemas',
191
+ '$dynamicRef'
192
+ ];
193
+ /**
194
+ * With no in-place applicator on the same schema, `unevaluatedProperties`
195
+ * means the same as `additionalProperties` (JSON Schema 2020-12 §11.3,
196
+ * §10.3.2.3), so rename it. When `additionalProperties` is also present it
197
+ * evaluates every remaining key, so `unevaluatedProperties` is dropped.
198
+ */
199
+ const normalizeUnevaluatedProperties = (schema) => {
200
+ if (!('unevaluatedProperties' in schema)) {
201
+ return schema;
202
+ }
203
+ if (inPlaceApplicators.some(keyword => keyword in schema)) {
204
+ return schema;
205
+ }
206
+ // `unevaluatedProperties` is a 3.1 keyword the 3.0-typed SchemaObject does
207
+ // not model; read it via an annotated destructure.
208
+ const { unevaluatedProperties, ...rest } = schema;
209
+ if ('additionalProperties' in rest) {
210
+ return rest;
211
+ }
212
+ if (!isAdditionalPropertiesValue(unevaluatedProperties)) {
213
+ return schema;
214
+ }
215
+ return { ...rest, additionalProperties: unevaluatedProperties };
216
+ };
217
+ /**
218
+ * Is this a value `additionalProperties` accepts: a boolean, or a schema or
219
+ * `$ref` object?
220
+ */
221
+ const isAdditionalPropertiesValue = (value) => typeof value === 'boolean' ||
222
+ (typeof value === 'object' && value !== null && !Array.isArray(value));
223
+ /**
224
+ * Remove `unevaluatedProperties` from an `allOf`/`oneOf`/`anyOf` wrapper and
225
+ * log it as skipped. Left in place, it would be renamed to
226
+ * `additionalProperties` when the merged or collapsed schema is dispatched
227
+ * again with no composition keyword left.
228
+ */
229
+ const withoutWrapperUnevaluatedProperties = ({ schema, parentType, stackTrail, context }) => {
230
+ if (!('unevaluatedProperties' in schema)) {
231
+ return schema;
232
+ }
233
+ const { unevaluatedProperties, ...rest } = schema;
234
+ context.logSkippedFields({
235
+ skipped: { unevaluatedProperties },
236
+ parent: schema,
237
+ parentType,
238
+ stackTrail
239
+ });
240
+ return rest;
241
+ };
179
242
  /**
180
243
  * Map the 3.1-only schema-shape encodings onto the shared IR shape before
181
- * dispatching: type arrays, `const`, `examples[]`, numeric exclusive bounds,
182
- * and binary/base64 content keywords. Each step returns its input unchanged
183
- * when it does not apply, so the composed result is referentially equal to
184
- * `schema` when nothing matched — letting the dispatcher re-dispatch only when
185
- * needed.
244
+ * dispatching: `unevaluatedProperties`, type arrays, `const`, `examples[]`,
245
+ * numeric exclusive bounds, and binary/base64 content keywords. Each step
246
+ * returns its input unchanged when it does not apply, so the composed result
247
+ * is referentially equal to `schema` when nothing matched — letting the
248
+ * dispatcher re-dispatch only when needed.
186
249
  */
187
250
  const normalizeSchemaShape = (schema) => {
188
- return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(schema)))));
251
+ return normalizeExclusiveBounds(normalizeExamples(normalizeConst(normalizeContentFormat(normalizeTypeArray(normalizeUnevaluatedProperties(schema))))));
189
252
  };
190
253
  /**
191
254
  * Is this a 3.1 `{ type: 'null' }` schema member? (`'null'` is a 3.1 type
@@ -257,6 +320,12 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
257
320
  return (0, exports.toSchemaV3)({ schema: normalized, stackTrail, context });
258
321
  }
259
322
  if ('allOf' in schema && Array.isArray(schema.allOf)) {
323
+ const wrapper = withoutWrapperUnevaluatedProperties({
324
+ schema,
325
+ parentType: 'allOf',
326
+ stackTrail,
327
+ context
328
+ });
260
329
  return stackTrail.trace('allOf', st => {
261
330
  // A single-member allOf collapses to its sole member — the value must
262
331
  // satisfy exactly that one schema. When the member is a `$ref`, keep it
@@ -266,7 +335,7 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
266
335
  // self-referential, and eagerly resolving it would not terminate. The
267
336
  // ref resolves at use time like every other ref. Mirrors the
268
337
  // single-member oneOf/anyOf handling below.
269
- const { allOf, ...value } = schema;
338
+ const { allOf, ...value } = wrapper;
270
339
  const members = allOf ?? [];
271
340
  if (members.length === 1) {
272
341
  const [member] = members;
@@ -286,20 +355,26 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
286
355
  });
287
356
  }
288
357
  const merged = (0, merge_intersection_js_1.mergeIntersection)({
289
- schema,
358
+ schema: wrapper,
290
359
  getRef: (0, refFns_js_2.toGetRef)(context.documentObject)
291
360
  });
292
361
  return (0, exports.toSchemaV3)({ schema: merged, stackTrail: st, context });
293
362
  });
294
363
  }
295
364
  if ('oneOf' in schema && Array.isArray(schema.oneOf)) {
365
+ const wrapper = withoutWrapperUnevaluatedProperties({
366
+ schema,
367
+ parentType: 'oneOf',
368
+ stackTrail,
369
+ context
370
+ });
296
371
  return stackTrail.trace('oneOf', st => {
297
372
  // 3.1: a `{ type: 'null' }` member makes the union nullable. Fold it
298
373
  // into a `nullable` flag the collapse/union logic already understands.
299
- const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.oneOf ?? []);
374
+ const { members: oneOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.oneOf ?? []);
300
375
  const withoutNull = hasNullMember
301
- ? { ...schema, oneOf: oneOfWithoutNull, nullable: true }
302
- : schema;
376
+ ? { ...wrapper, oneOf: oneOfWithoutNull, nullable: true }
377
+ : wrapper;
303
378
  const merged = (0, merge_union_js_1.mergeUnion)({
304
379
  schema: withoutNull,
305
380
  getRef: (0, refFns_js_2.toGetRef)(context.documentObject),
@@ -342,19 +417,25 @@ const toSchemaV3 = ({ schema, stackTrail, context }) => {
342
417
  });
343
418
  }
344
419
  if ('anyOf' in schema && Array.isArray(schema.anyOf)) {
420
+ const wrapper = withoutWrapperUnevaluatedProperties({
421
+ schema,
422
+ parentType: 'anyOf',
423
+ stackTrail,
424
+ context
425
+ });
345
426
  return stackTrail.trace('anyOf', st => {
346
427
  // deno-lint-ignore ban-ts-comment
347
428
  // @ts-expect-error
348
- if (schema['x-expansionResources'] && Array.isArray(schema.anyOf)) {
349
- const { anyOf, ...value } = schema;
429
+ if (wrapper['x-expansionResources'] && Array.isArray(wrapper.anyOf)) {
430
+ const { anyOf, ...value } = wrapper;
350
431
  return (0, toUnion_js_1.toUnion)({ value, members: anyOf, parentType: 'anyOf', stackTrail: st, context });
351
432
  }
352
433
  // 3.1: a `{ type: 'null' }` member makes the union nullable (mirrors
353
434
  // the oneOf branch above).
354
- const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(schema.anyOf ?? []);
435
+ const { members: anyOfWithoutNull, nullable: hasNullMember } = partitionNullMember(wrapper.anyOf ?? []);
355
436
  const withoutNull = hasNullMember
356
- ? { ...schema, anyOf: anyOfWithoutNull, nullable: true }
357
- : schema;
437
+ ? { ...wrapper, anyOf: anyOfWithoutNull, nullable: true }
438
+ : wrapper;
358
439
  const merged = (0, merge_union_js_1.mergeUnion)({
359
440
  schema: withoutNull,
360
441
  getRef: (0, refFns_js_2.toGetRef)(context.documentObject),