@schema-ts/core 0.1.3 → 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(
@@ -1438,6 +1438,13 @@ function resolveEffectiveSchema(validator, schema, value, keywordLocation, insta
1438
1438
  } else {
1439
1439
  type = detectSchemaType(value);
1440
1440
  }
1441
+ if (!isRequired && value === void 0) {
1442
+ return {
1443
+ effectiveSchema: effective,
1444
+ type,
1445
+ error: void 0
1446
+ };
1447
+ }
1441
1448
  const validationOutput = validator.validate(
1442
1449
  effective,
1443
1450
  value,
@@ -1780,16 +1787,17 @@ function getSubSchema(schema, key) {
1780
1787
  }
1781
1788
 
1782
1789
  // src/default.ts
1783
- function getDefaultValue(schema, value, strategy = "explicit") {
1790
+ function getDefaultValue(schema, options = {}) {
1791
+ const { value, strategy = "explicit" } = options;
1784
1792
  if (value === void 0) {
1785
1793
  if (schema.const !== void 0) return schema.const;
1786
1794
  if (schema.default !== void 0) return schema.default;
1787
- if (strategy === "explicit") return void 0;
1788
1795
  }
1789
1796
  const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
1790
1797
  if (type === "object" || !type && schema.properties) {
1791
1798
  let obj;
1792
1799
  if (value === void 0) {
1800
+ if (strategy === "explicit") return void 0;
1793
1801
  obj = {};
1794
1802
  } else if (typeof value === "object" && value !== null) {
1795
1803
  obj = value;
@@ -1799,9 +1807,9 @@ function getDefaultValue(schema, value, strategy = "explicit") {
1799
1807
  if (schema.properties) {
1800
1808
  for (const [key, subschema] of Object.entries(schema.properties)) {
1801
1809
  if (obj[key] !== void 0) {
1802
- obj[key] = getDefaultValue(subschema, obj[key], strategy);
1810
+ obj[key] = getDefaultValue(subschema, { value: obj[key], strategy });
1803
1811
  } else if (schema.required?.includes(key)) {
1804
- obj[key] = getDefaultValue(subschema, void 0, strategy);
1812
+ obj[key] = getDefaultValue(subschema, { strategy });
1805
1813
  }
1806
1814
  }
1807
1815
  }
@@ -1810,6 +1818,7 @@ function getDefaultValue(schema, value, strategy = "explicit") {
1810
1818
  if (type === "array") {
1811
1819
  let arr;
1812
1820
  if (value === void 0) {
1821
+ if (strategy === "explicit") return void 0;
1813
1822
  arr = [];
1814
1823
  } else if (Array.isArray(value)) {
1815
1824
  arr = value;
@@ -1819,16 +1828,19 @@ function getDefaultValue(schema, value, strategy = "explicit") {
1819
1828
  if (schema.prefixItems) {
1820
1829
  schema.prefixItems.forEach((subschema, index) => {
1821
1830
  if (index < arr.length) {
1822
- arr[index] = getDefaultValue(subschema, arr[index], strategy);
1831
+ arr[index] = getDefaultValue(subschema, {
1832
+ value: arr[index],
1833
+ strategy
1834
+ });
1823
1835
  } else if (value === void 0) {
1824
- arr.push(getDefaultValue(subschema, void 0, strategy));
1836
+ arr.push(getDefaultValue(subschema, { strategy }));
1825
1837
  }
1826
1838
  });
1827
1839
  }
1828
1840
  if (value !== void 0 && schema.items) {
1829
1841
  const startIndex = schema.prefixItems ? schema.prefixItems.length : 0;
1830
1842
  for (let i = startIndex; i < arr.length; i++) {
1831
- arr[i] = getDefaultValue(schema.items, arr[i], strategy);
1843
+ arr[i] = getDefaultValue(schema.items, { value: arr[i], strategy });
1832
1844
  }
1833
1845
  }
1834
1846
  return arr;
@@ -1836,6 +1848,9 @@ function getDefaultValue(schema, value, strategy = "explicit") {
1836
1848
  if (value !== void 0) {
1837
1849
  return value;
1838
1850
  }
1851
+ if (strategy === "explicit") {
1852
+ return void 0;
1853
+ }
1839
1854
  switch (type) {
1840
1855
  case "string":
1841
1856
  return "";
@@ -2059,7 +2074,11 @@ var SchemaRuntime = class {
2059
2074
  constructor(validator, schema, value, options = {}) {
2060
2075
  this.validator = validator;
2061
2076
  this.value = value;
2062
- this.options = { autoFillDefaults: "explicit", ...options };
2077
+ this.options = {
2078
+ autoFillDefaults: "explicit",
2079
+ removeEmptyContainers: "auto",
2080
+ ...options
2081
+ };
2063
2082
  const normalized = normalizeSchema(schema);
2064
2083
  this.rootSchema = dereferenceSchemaDeep(normalized, normalized);
2065
2084
  this.root = this.createEmptyNode("", "#");
@@ -2113,12 +2132,15 @@ var SchemaRuntime = class {
2113
2132
  return {
2114
2133
  type: "null",
2115
2134
  schema: {},
2135
+ // Placeholder, will be set in buildNode
2116
2136
  version: -1,
2137
+ // -1 indicates initial construction (not yet built)
2117
2138
  instanceLocation,
2118
2139
  keywordLocation,
2119
2140
  originalSchema: {},
2120
2141
  canRemove: false,
2121
2142
  canAdd: false,
2143
+ isRequired: false,
2122
2144
  children: []
2123
2145
  };
2124
2146
  }
@@ -2256,9 +2278,29 @@ var SchemaRuntime = class {
2256
2278
  }
2257
2279
  return setJsonPointer(this.value, normalizedPath, value);
2258
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
+ }
2259
2300
  /**
2260
2301
  * Remove a node at the specified path.
2261
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.
2262
2304
  * @param path - The path to remove
2263
2305
  * @returns true if successful, false if the path cannot be removed
2264
2306
  */
@@ -2271,15 +2313,54 @@ var SchemaRuntime = class {
2271
2313
  if (!node || !node.canRemove) {
2272
2314
  return false;
2273
2315
  }
2274
- 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);
2275
2343
  if (!success) {
2276
- return false;
2344
+ return path;
2277
2345
  }
2278
- const lastSlash = normalizedPath.lastIndexOf("/");
2279
- const parentPath = lastSlash <= 0 ? ROOT_PATH : normalizedPath.substring(0, lastSlash);
2280
- this.reconcile(parentPath);
2281
- this.notify({ type: "value", path: parentPath });
2282
- 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;
2283
2364
  }
2284
2365
  /**
2285
2366
  * Add a new child to an array or object at the specified parent path.
@@ -2322,7 +2403,7 @@ var SchemaRuntime = class {
2322
2403
  if (!keywordLocationToken) {
2323
2404
  return false;
2324
2405
  }
2325
- const defaultValue = initialValue !== void 0 ? initialValue : getDefaultValue(subschema);
2406
+ const defaultValue = initialValue !== void 0 ? initialValue : this.getDefaultValueForAdd(subschema);
2326
2407
  const itemPath = jsonPointerJoin(normalizedPath, String(newIndex));
2327
2408
  const success = setJsonPointer(this.value, itemPath, defaultValue);
2328
2409
  if (!success) return false;
@@ -2340,7 +2421,7 @@ var SchemaRuntime = class {
2340
2421
  if (!keywordLocationToken) {
2341
2422
  return false;
2342
2423
  }
2343
- const defaultValue = initialValue !== void 0 ? initialValue : getDefaultValue(subschema);
2424
+ const defaultValue = initialValue !== void 0 ? initialValue : this.getDefaultValueForAdd(subschema);
2344
2425
  const propertyPath = jsonPointerJoin(normalizedPath, key);
2345
2426
  const success = setJsonPointer(this.value, propertyPath, defaultValue);
2346
2427
  if (!success) return false;
@@ -2355,16 +2436,26 @@ var SchemaRuntime = class {
2355
2436
  * Creates intermediate containers (objects/arrays) as needed.
2356
2437
  * Triggers reconciliation and notifies subscribers.
2357
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
+ *
2358
2442
  * @param path - The JSON Pointer path (e.g., "/user/name", "" for root)
2359
- * @param value - The new value to set
2443
+ * @param value - The new value to set. If undefined and field is optional, removes the field.
2360
2444
  * @returns true if successful, false if the path cannot be set
2361
2445
  *
2362
2446
  * @example
2363
2447
  * runtime.setValue("/name", "Bob"); // set name to "Bob"
2364
2448
  * runtime.setValue("", { name: "Alice" }); // replace entire root value
2449
+ * runtime.setValue("/optional", undefined); // remove optional field
2365
2450
  */
2366
2451
  setValue(path, value) {
2367
2452
  const normalizedPath = normalizeRootPath(path);
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
+ }
2458
+ }
2368
2459
  const success = this.setValueAtPath(normalizedPath, value);
2369
2460
  if (!success) return false;
2370
2461
  this.reconcile(normalizedPath);
@@ -2430,29 +2521,56 @@ var SchemaRuntime = class {
2430
2521
  * This handles cases like if-then-else where new properties with defaults
2431
2522
  * may appear when conditions change.
2432
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
+ *
2433
2528
  * @param instanceLocation - The path to the node
2434
2529
  * @param newSchema - The new effective schema
2435
2530
  * @param type - The schema type
2531
+ * @param isRequired - Whether this node is required by its parent
2436
2532
  */
2437
- applySchemaDefaults(instanceLocation, newSchema, type) {
2438
- if (this.options.autoFillDefaults === "never") {
2533
+ applySchemaDefaults(instanceLocation, newSchema, type, isRequired = true) {
2534
+ const strategy = this.options.autoFillDefaults;
2535
+ if (strategy === "never") {
2439
2536
  return;
2440
2537
  }
2441
- const value = this.getValue(instanceLocation);
2538
+ let value = this.getValue(instanceLocation);
2539
+ const isRoot = instanceLocation === ROOT_PATH;
2442
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
+ }
2443
2558
  const obj = value && typeof value === "object" ? value : null;
2444
2559
  if (!obj) return;
2445
2560
  for (const [key, subschema] of Object.entries(newSchema.properties)) {
2446
2561
  const hasValue = obj[key] !== void 0;
2562
+ const isChildRequired = requiredSet.has(key);
2447
2563
  if (!hasValue) {
2448
- const defaultValue = getDefaultValue(
2449
- subschema,
2450
- void 0,
2451
- this.options.autoFillDefaults
2452
- );
2564
+ const defaultValue = getDefaultValue(subschema, { strategy });
2453
2565
  if (defaultValue !== void 0) {
2454
2566
  const propertyPath = jsonPointerJoin(instanceLocation, key);
2455
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, []);
2456
2574
  }
2457
2575
  }
2458
2576
  }
@@ -2464,11 +2582,7 @@ var SchemaRuntime = class {
2464
2582
  for (let i = 0; i < newSchema.prefixItems.length; i++) {
2465
2583
  if (arr[i] === void 0) {
2466
2584
  const itemSchema = newSchema.prefixItems[i];
2467
- const defaultValue = getDefaultValue(
2468
- itemSchema,
2469
- void 0,
2470
- this.options.autoFillDefaults
2471
- );
2585
+ const defaultValue = getDefaultValue(itemSchema, { strategy });
2472
2586
  if (defaultValue !== void 0) {
2473
2587
  const itemPath = jsonPointerJoin(instanceLocation, String(i));
2474
2588
  setJsonPointer(this.value, itemPath, defaultValue);
@@ -2492,7 +2606,7 @@ var SchemaRuntime = class {
2492
2606
  }
2493
2607
  }
2494
2608
  const newChildren = [];
2495
- const processChild = (childKey, childSchema, childkeywordLocation, canRemove = false) => {
2609
+ const processChild = (childKey, childSchema, childkeywordLocation, canRemove = false, isRequired = false) => {
2496
2610
  const childinstanceLocation = jsonPointerJoin(instanceLocation, childKey);
2497
2611
  let childNode = oldChildrenMap.get(childinstanceLocation);
2498
2612
  if (childNode) {
@@ -2505,7 +2619,8 @@ var SchemaRuntime = class {
2505
2619
  );
2506
2620
  }
2507
2621
  childNode.canRemove = canRemove;
2508
- this.buildNode(childNode, childSchema, options);
2622
+ childNode.isRequired = isRequired;
2623
+ this.buildNode(childNode, childSchema, { ...options, isRequired });
2509
2624
  newChildren.push(childNode);
2510
2625
  };
2511
2626
  switch (type) {
@@ -2518,11 +2633,13 @@ var SchemaRuntime = class {
2518
2633
  effectiveSchema.properties
2519
2634
  )) {
2520
2635
  processedKeys.add(key);
2636
+ const isChildRequired = effectiveSchema.required?.includes(key) ?? false;
2521
2637
  processChild(
2522
2638
  key,
2523
2639
  subschema,
2524
2640
  `${keywordLocation}/properties/${key}`,
2525
- false
2641
+ false,
2642
+ isChildRequired
2526
2643
  );
2527
2644
  }
2528
2645
  }
@@ -2537,7 +2654,9 @@ var SchemaRuntime = class {
2537
2654
  key,
2538
2655
  subschema,
2539
2656
  `${keywordLocation}/patternProperties/${jsonPointerEscape(pattern)}`,
2540
- true
2657
+ true,
2658
+ false
2659
+ // patternProperties are never required
2541
2660
  );
2542
2661
  }
2543
2662
  }
@@ -2551,7 +2670,9 @@ var SchemaRuntime = class {
2551
2670
  key,
2552
2671
  subschema,
2553
2672
  `${keywordLocation}/additionalProperties`,
2554
- true
2673
+ true,
2674
+ false
2675
+ // additionalProperties are never required
2555
2676
  );
2556
2677
  }
2557
2678
  }
@@ -2569,7 +2690,9 @@ var SchemaRuntime = class {
2569
2690
  String(i),
2570
2691
  effectiveSchema.prefixItems[i],
2571
2692
  `${keywordLocation}/prefixItems/${i}`,
2572
- false
2693
+ false,
2694
+ true
2695
+ // array items are always considered required
2573
2696
  );
2574
2697
  }
2575
2698
  }
@@ -2579,7 +2702,9 @@ var SchemaRuntime = class {
2579
2702
  String(i),
2580
2703
  effectiveSchema.items,
2581
2704
  `${keywordLocation}/items`,
2705
+ true,
2582
2706
  true
2707
+ // array items are always considered required
2583
2708
  );
2584
2709
  }
2585
2710
  }
@@ -2596,6 +2721,7 @@ var SchemaRuntime = class {
2596
2721
  * Build/update a FieldNode in place.
2597
2722
  * Updates the node's schema, type, error, and children based on the current value.
2598
2723
  * @param schema - Optional. If provided, updates node.originalSchema. Otherwise uses existing.
2724
+ * @param isRequired - Whether this node is required by its parent schema.
2599
2725
  */
2600
2726
  buildNode(node, schema, options = {}) {
2601
2727
  const { keywordLocation, instanceLocation } = node;
@@ -2613,17 +2739,25 @@ var SchemaRuntime = class {
2613
2739
  if (schemaChanged) {
2614
2740
  this.updateNodeDependencies(node, schema);
2615
2741
  }
2742
+ const isRequired = options.isRequired ?? true;
2616
2743
  const { type, effectiveSchema, error } = resolveEffectiveSchema(
2617
2744
  this.validator,
2618
2745
  node.originalSchema,
2619
2746
  value,
2620
2747
  keywordLocation,
2621
- instanceLocation
2748
+ instanceLocation,
2749
+ isRequired
2622
2750
  );
2623
- const effectiveSchemaChanged = !deepEqual(effectiveSchema, node.schema) || type !== node.type;
2624
- 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);
2625
2754
  if (effectiveSchemaChanged) {
2626
- this.applySchemaDefaults(instanceLocation, effectiveSchema, type);
2755
+ this.applySchemaDefaults(
2756
+ instanceLocation,
2757
+ effectiveSchema,
2758
+ type,
2759
+ isRequired
2760
+ );
2627
2761
  }
2628
2762
  node.schema = effectiveSchema;
2629
2763
  node.type = type;