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