@schema-ts/core 0.1.2 → 0.1.4

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
@@ -1334,7 +1334,7 @@ function validateSchema(schema, value, instancePath = "", schemaPath = "#", fast
1334
1334
  }
1335
1335
 
1336
1336
  // src/effective.ts
1337
- function resolveEffectiveSchema(validator, schema, value, keywordLocation, instanceLocation) {
1337
+ function resolveEffectiveSchema(validator, schema, value, keywordLocation, instanceLocation, isRequired = true) {
1338
1338
  let effective = schema;
1339
1339
  if (effective.if) {
1340
1340
  const output = validator.validate(
@@ -1400,7 +1400,6 @@ function resolveEffectiveSchema(validator, schema, value, keywordLocation, insta
1400
1400
  instanceLocation
1401
1401
  );
1402
1402
  effective = mergeSchema(effective, res.effectiveSchema);
1403
- break;
1404
1403
  }
1405
1404
  }
1406
1405
  const { anyOf: _, ...rest } = effective;
@@ -1439,6 +1438,13 @@ function resolveEffectiveSchema(validator, schema, value, keywordLocation, insta
1439
1438
  } else {
1440
1439
  type = detectSchemaType(value);
1441
1440
  }
1441
+ if (!isRequired && value === void 0) {
1442
+ return {
1443
+ effectiveSchema: effective,
1444
+ type,
1445
+ error: void 0
1446
+ };
1447
+ }
1442
1448
  const validationOutput = validator.validate(
1443
1449
  effective,
1444
1450
  value,
@@ -1472,6 +1478,19 @@ function mergeSchemaArrays(a, b) {
1472
1478
  if (b === void 0) return a;
1473
1479
  return [...a, ...b];
1474
1480
  }
1481
+ function mergeSchemaMap(base, override) {
1482
+ if (base === void 0) return override;
1483
+ if (override === void 0) return base;
1484
+ const merged = { ...base };
1485
+ for (const [key, schema] of Object.entries(override)) {
1486
+ if (merged[key]) {
1487
+ merged[key] = mergeSchema(merged[key], schema);
1488
+ } else {
1489
+ merged[key] = schema;
1490
+ }
1491
+ }
1492
+ return merged;
1493
+ }
1475
1494
  function mergeSchema(base, override) {
1476
1495
  if (!override) return base;
1477
1496
  const merged = {
@@ -1498,17 +1517,16 @@ function mergeSchema(base, override) {
1498
1517
  ...override.dependentRequired
1499
1518
  };
1500
1519
  }
1501
- if (base.properties || override.properties) {
1502
- merged.properties = {
1503
- ...base.properties,
1504
- ...override.properties
1505
- };
1520
+ const mergedProperties = mergeSchemaMap(base.properties, override.properties);
1521
+ if (mergedProperties !== void 0) {
1522
+ merged.properties = mergedProperties;
1506
1523
  }
1507
- if (base.patternProperties || override.patternProperties) {
1508
- merged.patternProperties = {
1509
- ...base.patternProperties,
1510
- ...override.patternProperties
1511
- };
1524
+ const mergedPatternProperties = mergeSchemaMap(
1525
+ base.patternProperties,
1526
+ override.patternProperties
1527
+ );
1528
+ if (mergedPatternProperties !== void 0) {
1529
+ merged.patternProperties = mergedPatternProperties;
1512
1530
  }
1513
1531
  if (base.items && override.items) {
1514
1532
  merged.items = mergeSchema(base.items, override.items);
@@ -1543,11 +1561,12 @@ function mergeSchema(base, override) {
1543
1561
  merged[keyword] = mergedArray;
1544
1562
  }
1545
1563
  }
1546
- if (base.dependentSchemas || override.dependentSchemas) {
1547
- merged.dependentSchemas = {
1548
- ...base.dependentSchemas,
1549
- ...override.dependentSchemas
1550
- };
1564
+ const mergedDependentSchemas = mergeSchemaMap(
1565
+ base.dependentSchemas,
1566
+ override.dependentSchemas
1567
+ );
1568
+ if (mergedDependentSchemas !== void 0) {
1569
+ merged.dependentSchemas = mergedDependentSchemas;
1551
1570
  }
1552
1571
  return merged;
1553
1572
  }
@@ -1768,7 +1787,8 @@ function getSubSchema(schema, key) {
1768
1787
  }
1769
1788
 
1770
1789
  // src/default.ts
1771
- function getDefaultValue(schema, value) {
1790
+ function getDefaultValue(schema, options = {}) {
1791
+ const { value, strategy = "explicit" } = options;
1772
1792
  if (value === void 0) {
1773
1793
  if (schema.const !== void 0) return schema.const;
1774
1794
  if (schema.default !== void 0) return schema.default;
@@ -1777,6 +1797,7 @@ function getDefaultValue(schema, value) {
1777
1797
  if (type === "object" || !type && schema.properties) {
1778
1798
  let obj;
1779
1799
  if (value === void 0) {
1800
+ if (strategy === "explicit") return void 0;
1780
1801
  obj = {};
1781
1802
  } else if (typeof value === "object" && value !== null) {
1782
1803
  obj = value;
@@ -1786,9 +1807,9 @@ function getDefaultValue(schema, value) {
1786
1807
  if (schema.properties) {
1787
1808
  for (const [key, subschema] of Object.entries(schema.properties)) {
1788
1809
  if (obj[key] !== void 0) {
1789
- obj[key] = getDefaultValue(subschema, obj[key]);
1810
+ obj[key] = getDefaultValue(subschema, { value: obj[key], strategy });
1790
1811
  } else if (schema.required?.includes(key)) {
1791
- obj[key] = getDefaultValue(subschema);
1812
+ obj[key] = getDefaultValue(subschema, { strategy });
1792
1813
  }
1793
1814
  }
1794
1815
  }
@@ -1797,6 +1818,7 @@ function getDefaultValue(schema, value) {
1797
1818
  if (type === "array") {
1798
1819
  let arr;
1799
1820
  if (value === void 0) {
1821
+ if (strategy === "explicit") return void 0;
1800
1822
  arr = [];
1801
1823
  } else if (Array.isArray(value)) {
1802
1824
  arr = value;
@@ -1806,16 +1828,19 @@ function getDefaultValue(schema, value) {
1806
1828
  if (schema.prefixItems) {
1807
1829
  schema.prefixItems.forEach((subschema, index) => {
1808
1830
  if (index < arr.length) {
1809
- arr[index] = getDefaultValue(subschema, arr[index]);
1831
+ arr[index] = getDefaultValue(subschema, {
1832
+ value: arr[index],
1833
+ strategy
1834
+ });
1810
1835
  } else if (value === void 0) {
1811
- arr.push(getDefaultValue(subschema));
1836
+ arr.push(getDefaultValue(subschema, { strategy }));
1812
1837
  }
1813
1838
  });
1814
1839
  }
1815
1840
  if (value !== void 0 && schema.items) {
1816
1841
  const startIndex = schema.prefixItems ? schema.prefixItems.length : 0;
1817
1842
  for (let i = startIndex; i < arr.length; i++) {
1818
- arr[i] = getDefaultValue(schema.items, arr[i]);
1843
+ arr[i] = getDefaultValue(schema.items, { value: arr[i], strategy });
1819
1844
  }
1820
1845
  }
1821
1846
  return arr;
@@ -1823,6 +1848,9 @@ function getDefaultValue(schema, value) {
1823
1848
  if (value !== void 0) {
1824
1849
  return value;
1825
1850
  }
1851
+ if (strategy === "explicit") {
1852
+ return void 0;
1853
+ }
1826
1854
  switch (type) {
1827
1855
  case "string":
1828
1856
  return "";
@@ -2029,21 +2057,28 @@ var SchemaRuntime = class {
2029
2057
  value;
2030
2058
  version = 0;
2031
2059
  rootSchema = {};
2060
+ options;
2032
2061
  /**
2033
2062
  * Create a new SchemaRuntime instance.
2034
2063
  *
2035
2064
  * @param validator - The validator instance for schema validation
2036
2065
  * @param schema - The JSON Schema definition (will be normalized and dereferenced)
2037
2066
  * @param value - The initial data value to manage
2067
+ * @param options - Runtime configuration options
2038
2068
  *
2039
2069
  * @example
2040
2070
  * const validator = new Validator();
2041
2071
  * const schema = { type: "object", properties: { name: { type: "string" } } };
2042
2072
  * const runtime = new SchemaRuntime(validator, schema, { name: "Alice" });
2043
2073
  */
2044
- constructor(validator, schema, value) {
2074
+ constructor(validator, schema, value, options = {}) {
2045
2075
  this.validator = validator;
2046
2076
  this.value = value;
2077
+ this.options = {
2078
+ autoFillDefaults: "explicit",
2079
+ removeEmptyContainers: "auto",
2080
+ ...options
2081
+ };
2047
2082
  const normalized = normalizeSchema(schema);
2048
2083
  this.rootSchema = dereferenceSchemaDeep(normalized, normalized);
2049
2084
  this.root = this.createEmptyNode("", "#");
@@ -2097,12 +2132,15 @@ var SchemaRuntime = class {
2097
2132
  return {
2098
2133
  type: "null",
2099
2134
  schema: {},
2135
+ // Placeholder, will be set in buildNode
2100
2136
  version: -1,
2137
+ // -1 indicates initial construction (not yet built)
2101
2138
  instanceLocation,
2102
2139
  keywordLocation,
2103
2140
  originalSchema: {},
2104
2141
  canRemove: false,
2105
2142
  canAdd: false,
2143
+ isRequired: false,
2106
2144
  children: []
2107
2145
  };
2108
2146
  }
@@ -2225,9 +2263,44 @@ var SchemaRuntime = class {
2225
2263
  if (normalizedPath === ROOT_PATH) return this.value;
2226
2264
  return getJsonPointer(this.value, normalizedPath);
2227
2265
  }
2266
+ /**
2267
+ * Internal helper to set a value at a normalized path.
2268
+ * Handles both root and non-root paths.
2269
+ *
2270
+ * @param normalizedPath - The normalized JSON Pointer path
2271
+ * @param value - The value to set
2272
+ * @returns true if successful, false if the path cannot be set
2273
+ */
2274
+ setValueAtPath(normalizedPath, value) {
2275
+ if (normalizedPath === ROOT_PATH) {
2276
+ this.value = value;
2277
+ return true;
2278
+ }
2279
+ return setJsonPointer(this.value, normalizedPath, value);
2280
+ }
2281
+ /**
2282
+ * Internal method to remove a value at a path.
2283
+ * Shared logic for removeValue and setValue(undefined).
2284
+ * @param path - The normalized path to remove
2285
+ * @param canRemove - Whether the removal is allowed (pre-checked by caller)
2286
+ * @returns true if successful, false if removal failed
2287
+ */
2288
+ removeValueInternal(path) {
2289
+ const success = removeJsonPointer(this.value, path);
2290
+ if (!success) {
2291
+ return false;
2292
+ }
2293
+ const lastSlash = path.lastIndexOf("/");
2294
+ const parentPath = lastSlash <= 0 ? ROOT_PATH : path.substring(0, lastSlash);
2295
+ const reconcilePath = this.cleanupEmptyContainers(parentPath);
2296
+ this.reconcile(reconcilePath);
2297
+ this.notify({ type: "value", path: reconcilePath });
2298
+ return true;
2299
+ }
2228
2300
  /**
2229
2301
  * Remove a node at the specified path.
2230
2302
  * This deletes the value from the data structure (array splice or object delete).
2303
+ * After removal, may also remove empty parent containers based on removeEmptyContainers option.
2231
2304
  * @param path - The path to remove
2232
2305
  * @returns true if successful, false if the path cannot be removed
2233
2306
  */
@@ -2240,15 +2313,54 @@ var SchemaRuntime = class {
2240
2313
  if (!node || !node.canRemove) {
2241
2314
  return false;
2242
2315
  }
2243
- const success = removeJsonPointer(this.value, normalizedPath);
2316
+ return this.removeValueInternal(normalizedPath);
2317
+ }
2318
+ /**
2319
+ * Clean up empty parent containers after element removal.
2320
+ * Recursively removes empty arrays/objects based on removeEmptyContainers option.
2321
+ * @param path - The path to check for empty container
2322
+ * @returns The topmost parent path for reconciliation
2323
+ */
2324
+ cleanupEmptyContainers(path) {
2325
+ const strategy = this.options.removeEmptyContainers;
2326
+ if (strategy === "never" || path === ROOT_PATH) {
2327
+ return path;
2328
+ }
2329
+ const node = this.findNode(path);
2330
+ if (!node) {
2331
+ return path;
2332
+ }
2333
+ const value = this.getValue(path);
2334
+ const isEmpty = Array.isArray(value) && value.length === 0 || value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
2335
+ if (!isEmpty) {
2336
+ return path;
2337
+ }
2338
+ const shouldRemove = strategy === "always" || strategy === "auto" && !node.isRequired;
2339
+ if (!shouldRemove) {
2340
+ return path;
2341
+ }
2342
+ const success = removeJsonPointer(this.value, path);
2244
2343
  if (!success) {
2245
- return false;
2344
+ return path;
2246
2345
  }
2247
- const lastSlash = normalizedPath.lastIndexOf("/");
2248
- const parentPath = lastSlash <= 0 ? ROOT_PATH : normalizedPath.substring(0, lastSlash);
2249
- this.reconcile(parentPath);
2250
- this.notify({ type: "value", path: parentPath });
2251
- return true;
2346
+ const lastSlash = path.lastIndexOf("/");
2347
+ const parentPath = lastSlash <= 0 ? ROOT_PATH : path.substring(0, lastSlash);
2348
+ return this.cleanupEmptyContainers(parentPath);
2349
+ }
2350
+ /**
2351
+ * Get default value for a schema, respecting autoFillDefaults option.
2352
+ * Falls back to 'always' strategy if configured strategy returns undefined.
2353
+ */
2354
+ getDefaultValueForAdd(schema) {
2355
+ const strategy = this.options.autoFillDefaults;
2356
+ let defaultValue;
2357
+ if (strategy && strategy !== "never") {
2358
+ defaultValue = getDefaultValue(schema, { strategy });
2359
+ }
2360
+ if (defaultValue === void 0) {
2361
+ defaultValue = getDefaultValue(schema, { strategy: "always" });
2362
+ }
2363
+ return defaultValue;
2252
2364
  }
2253
2365
  /**
2254
2366
  * Add a new child to an array or object at the specified parent path.
@@ -2265,8 +2377,23 @@ var SchemaRuntime = class {
2265
2377
  if (!parentNode || !parentNode.canAdd) {
2266
2378
  return false;
2267
2379
  }
2268
- const parentValue = this.getValue(normalizedPath);
2380
+ let parentValue = this.getValue(normalizedPath);
2269
2381
  const parentSchema = parentNode.schema;
2382
+ if (parentNode.type === "array") {
2383
+ if (Array.isArray(parentValue)) ; else if (parentValue === void 0 || parentValue === null) {
2384
+ parentValue = [];
2385
+ this.setValueAtPath(normalizedPath, parentValue);
2386
+ } else {
2387
+ return false;
2388
+ }
2389
+ } else if (parentNode.type === "object") {
2390
+ if (parentValue && typeof parentValue === "object") ; else if (parentValue === void 0 || parentValue === null) {
2391
+ parentValue = {};
2392
+ this.setValueAtPath(normalizedPath, parentValue);
2393
+ } else {
2394
+ return false;
2395
+ }
2396
+ }
2270
2397
  if (parentNode.type === "array" && Array.isArray(parentValue)) {
2271
2398
  const newIndex = parentValue.length;
2272
2399
  const { schema: subschema, keywordLocationToken } = getSubSchema(
@@ -2276,14 +2403,14 @@ var SchemaRuntime = class {
2276
2403
  if (!keywordLocationToken) {
2277
2404
  return false;
2278
2405
  }
2279
- const defaultValue = initialValue !== void 0 ? initialValue : getDefaultValue(subschema);
2406
+ const defaultValue = initialValue !== void 0 ? initialValue : this.getDefaultValueForAdd(subschema);
2280
2407
  const itemPath = jsonPointerJoin(normalizedPath, String(newIndex));
2281
2408
  const success = setJsonPointer(this.value, itemPath, defaultValue);
2282
2409
  if (!success) return false;
2283
2410
  this.reconcile(normalizedPath);
2284
2411
  this.notify({ type: "value", path: normalizedPath });
2285
2412
  return true;
2286
- } else if (parentNode.type === "object" && parentValue && typeof parentValue === "object") {
2413
+ } else if (parentNode.type === "object" && typeof parentValue === "object") {
2287
2414
  if (!key) {
2288
2415
  return false;
2289
2416
  }
@@ -2294,7 +2421,7 @@ var SchemaRuntime = class {
2294
2421
  if (!keywordLocationToken) {
2295
2422
  return false;
2296
2423
  }
2297
- const defaultValue = initialValue !== void 0 ? initialValue : getDefaultValue(subschema);
2424
+ const defaultValue = initialValue !== void 0 ? initialValue : this.getDefaultValueForAdd(subschema);
2298
2425
  const propertyPath = jsonPointerJoin(normalizedPath, key);
2299
2426
  const success = setJsonPointer(this.value, propertyPath, defaultValue);
2300
2427
  if (!success) return false;
@@ -2309,22 +2436,28 @@ var SchemaRuntime = class {
2309
2436
  * Creates intermediate containers (objects/arrays) as needed.
2310
2437
  * Triggers reconciliation and notifies subscribers.
2311
2438
  *
2439
+ * When value is undefined and the field is not required, the field will be
2440
+ * removed from the parent container (similar to removeValue behavior).
2441
+ *
2312
2442
  * @param path - The JSON Pointer path (e.g., "/user/name", "" for root)
2313
- * @param value - The new value to set
2443
+ * @param value - The new value to set. If undefined and field is optional, removes the field.
2314
2444
  * @returns true if successful, false if the path cannot be set
2315
2445
  *
2316
2446
  * @example
2317
2447
  * runtime.setValue("/name", "Bob"); // set name to "Bob"
2318
2448
  * runtime.setValue("", { name: "Alice" }); // replace entire root value
2449
+ * runtime.setValue("/optional", undefined); // remove optional field
2319
2450
  */
2320
2451
  setValue(path, value) {
2321
2452
  const normalizedPath = normalizeRootPath(path);
2322
- if (normalizedPath === ROOT_PATH) {
2323
- this.value = value;
2324
- } else {
2325
- const success = setJsonPointer(this.value, normalizedPath, value);
2326
- if (!success) return false;
2453
+ if (value === void 0 && normalizedPath !== ROOT_PATH) {
2454
+ const node = this.findNode(normalizedPath);
2455
+ if (node && !node.isRequired) {
2456
+ return this.removeValueInternal(normalizedPath);
2457
+ }
2327
2458
  }
2459
+ const success = this.setValueAtPath(normalizedPath, value);
2460
+ if (!success) return false;
2328
2461
  this.reconcile(normalizedPath);
2329
2462
  this.notify({ type: "value", path: normalizedPath });
2330
2463
  return true;
@@ -2388,22 +2521,56 @@ var SchemaRuntime = class {
2388
2521
  * This handles cases like if-then-else where new properties with defaults
2389
2522
  * may appear when conditions change.
2390
2523
  *
2524
+ * Container initialization rules:
2525
+ * - Root node is always considered required and will be initialized if it has defaults or required properties
2526
+ * - Nested containers are initialized only if they are in parent's required array
2527
+ *
2391
2528
  * @param instanceLocation - The path to the node
2392
2529
  * @param newSchema - The new effective schema
2393
2530
  * @param type - The schema type
2531
+ * @param isRequired - Whether this node is required by its parent
2394
2532
  */
2395
- applySchemaDefaults(instanceLocation, newSchema, type) {
2396
- const value = this.getValue(instanceLocation);
2533
+ applySchemaDefaults(instanceLocation, newSchema, type, isRequired = true) {
2534
+ const strategy = this.options.autoFillDefaults;
2535
+ if (strategy === "never") {
2536
+ return;
2537
+ }
2538
+ let value = this.getValue(instanceLocation);
2539
+ const isRoot = instanceLocation === ROOT_PATH;
2397
2540
  if (type === "object" && newSchema.properties) {
2541
+ const requiredSet = new Set(newSchema.required || []);
2542
+ const hasAnyDefaults = Object.entries(newSchema.properties).some(
2543
+ ([key, subschema]) => {
2544
+ const defaultValue = getDefaultValue(subschema, { strategy });
2545
+ if (defaultValue !== void 0) return true;
2546
+ const isChildRequired = requiredSet.has(key);
2547
+ if (isChildRequired && (subschema.type === "object" || subschema.type === "array")) {
2548
+ return true;
2549
+ }
2550
+ return false;
2551
+ }
2552
+ );
2553
+ const shouldInitialize = (value === void 0 || value === null) && hasAnyDefaults && (isRoot || isRequired);
2554
+ if (shouldInitialize) {
2555
+ value = {};
2556
+ this.setValueAtPath(normalizeRootPath(instanceLocation), value);
2557
+ }
2398
2558
  const obj = value && typeof value === "object" ? value : null;
2399
2559
  if (!obj) return;
2400
2560
  for (const [key, subschema] of Object.entries(newSchema.properties)) {
2401
2561
  const hasValue = obj[key] !== void 0;
2562
+ const isChildRequired = requiredSet.has(key);
2402
2563
  if (!hasValue) {
2403
- const defaultValue = getDefaultValue(subschema);
2564
+ const defaultValue = getDefaultValue(subschema, { strategy });
2404
2565
  if (defaultValue !== void 0) {
2405
2566
  const propertyPath = jsonPointerJoin(instanceLocation, key);
2406
2567
  setJsonPointer(this.value, propertyPath, defaultValue);
2568
+ } else if (isChildRequired && subschema.type === "object") {
2569
+ const propertyPath = jsonPointerJoin(instanceLocation, key);
2570
+ setJsonPointer(this.value, propertyPath, {});
2571
+ } else if (isChildRequired && subschema.type === "array") {
2572
+ const propertyPath = jsonPointerJoin(instanceLocation, key);
2573
+ setJsonPointer(this.value, propertyPath, []);
2407
2574
  }
2408
2575
  }
2409
2576
  }
@@ -2414,7 +2581,8 @@ var SchemaRuntime = class {
2414
2581
  if (!arr) return;
2415
2582
  for (let i = 0; i < newSchema.prefixItems.length; i++) {
2416
2583
  if (arr[i] === void 0) {
2417
- const defaultValue = getDefaultValue(newSchema.prefixItems[i]);
2584
+ const itemSchema = newSchema.prefixItems[i];
2585
+ const defaultValue = getDefaultValue(itemSchema, { strategy });
2418
2586
  if (defaultValue !== void 0) {
2419
2587
  const itemPath = jsonPointerJoin(instanceLocation, String(i));
2420
2588
  setJsonPointer(this.value, itemPath, defaultValue);
@@ -2438,7 +2606,7 @@ var SchemaRuntime = class {
2438
2606
  }
2439
2607
  }
2440
2608
  const newChildren = [];
2441
- const processChild = (childKey, childSchema, childkeywordLocation, canRemove = false) => {
2609
+ const processChild = (childKey, childSchema, childkeywordLocation, canRemove = false, isRequired = false) => {
2442
2610
  const childinstanceLocation = jsonPointerJoin(instanceLocation, childKey);
2443
2611
  let childNode = oldChildrenMap.get(childinstanceLocation);
2444
2612
  if (childNode) {
@@ -2451,7 +2619,8 @@ var SchemaRuntime = class {
2451
2619
  );
2452
2620
  }
2453
2621
  childNode.canRemove = canRemove;
2454
- this.buildNode(childNode, childSchema, options);
2622
+ childNode.isRequired = isRequired;
2623
+ this.buildNode(childNode, childSchema, { ...options, isRequired });
2455
2624
  newChildren.push(childNode);
2456
2625
  };
2457
2626
  switch (type) {
@@ -2464,11 +2633,13 @@ var SchemaRuntime = class {
2464
2633
  effectiveSchema.properties
2465
2634
  )) {
2466
2635
  processedKeys.add(key);
2636
+ const isChildRequired = effectiveSchema.required?.includes(key) ?? false;
2467
2637
  processChild(
2468
2638
  key,
2469
2639
  subschema,
2470
2640
  `${keywordLocation}/properties/${key}`,
2471
- false
2641
+ false,
2642
+ isChildRequired
2472
2643
  );
2473
2644
  }
2474
2645
  }
@@ -2483,7 +2654,9 @@ var SchemaRuntime = class {
2483
2654
  key,
2484
2655
  subschema,
2485
2656
  `${keywordLocation}/patternProperties/${jsonPointerEscape(pattern)}`,
2486
- true
2657
+ true,
2658
+ false
2659
+ // patternProperties are never required
2487
2660
  );
2488
2661
  }
2489
2662
  }
@@ -2497,7 +2670,9 @@ var SchemaRuntime = class {
2497
2670
  key,
2498
2671
  subschema,
2499
2672
  `${keywordLocation}/additionalProperties`,
2500
- true
2673
+ true,
2674
+ false
2675
+ // additionalProperties are never required
2501
2676
  );
2502
2677
  }
2503
2678
  }
@@ -2515,7 +2690,9 @@ var SchemaRuntime = class {
2515
2690
  String(i),
2516
2691
  effectiveSchema.prefixItems[i],
2517
2692
  `${keywordLocation}/prefixItems/${i}`,
2518
- false
2693
+ false,
2694
+ true
2695
+ // array items are always considered required
2519
2696
  );
2520
2697
  }
2521
2698
  }
@@ -2525,7 +2702,9 @@ var SchemaRuntime = class {
2525
2702
  String(i),
2526
2703
  effectiveSchema.items,
2527
2704
  `${keywordLocation}/items`,
2705
+ true,
2528
2706
  true
2707
+ // array items are always considered required
2529
2708
  );
2530
2709
  }
2531
2710
  }
@@ -2542,6 +2721,7 @@ var SchemaRuntime = class {
2542
2721
  * Build/update a FieldNode in place.
2543
2722
  * Updates the node's schema, type, error, and children based on the current value.
2544
2723
  * @param schema - Optional. If provided, updates node.originalSchema. Otherwise uses existing.
2724
+ * @param isRequired - Whether this node is required by its parent schema.
2545
2725
  */
2546
2726
  buildNode(node, schema, options = {}) {
2547
2727
  const { keywordLocation, instanceLocation } = node;
@@ -2559,17 +2739,25 @@ var SchemaRuntime = class {
2559
2739
  if (schemaChanged) {
2560
2740
  this.updateNodeDependencies(node, schema);
2561
2741
  }
2742
+ const isRequired = options.isRequired ?? true;
2562
2743
  const { type, effectiveSchema, error } = resolveEffectiveSchema(
2563
2744
  this.validator,
2564
2745
  node.originalSchema,
2565
2746
  value,
2566
2747
  keywordLocation,
2567
- instanceLocation
2748
+ instanceLocation,
2749
+ isRequired
2568
2750
  );
2569
- const effectiveSchemaChanged = !deepEqual(effectiveSchema, node.schema) || type !== node.type;
2570
- const errorChanged = !deepEqual(error, node.error);
2751
+ const isInitialBuild = node.version === -1;
2752
+ const effectiveSchemaChanged = isInitialBuild || !deepEqual(effectiveSchema, node.schema) || type !== node.type;
2753
+ const errorChanged = isInitialBuild || !deepEqual(error, node.error);
2571
2754
  if (effectiveSchemaChanged) {
2572
- this.applySchemaDefaults(instanceLocation, effectiveSchema, type);
2755
+ this.applySchemaDefaults(
2756
+ instanceLocation,
2757
+ effectiveSchema,
2758
+ type,
2759
+ isRequired
2760
+ );
2573
2761
  }
2574
2762
  node.schema = effectiveSchema;
2575
2763
  node.type = type;