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