@narrative.io/jsonforms-provider-protocols 3.0.0-beta.12 → 3.0.0-beta.14

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.
@@ -2,9 +2,16 @@
2
2
  * Initialize a form data object from a JSON Schema.
3
3
  * Resolves $ref, const, default, oneOf/discriminator, and typed empty values.
4
4
  *
5
+ * Optional fields (not listed in the parent schema's `required` array) that
6
+ * have no `const`, no single-value `enum`, and no `default` are omitted
7
+ * entirely from the result. This avoids seeding values (e.g. `null` for
8
+ * `type: "integer"`) that fail AJV's type check and surface spurious errors
9
+ * on untouched fields. Required fields retain legacy typed-empty seeding
10
+ * (`""`, `null`, `false`, `[]`) so that "is required" surfaces cleanly.
11
+ *
5
12
  * @param schema - The full JSON Schema (must include $defs if $refs are used)
6
13
  * @param seed - Optional existing data to merge (seed values take priority)
7
- * @returns A data object with all schema-defined fields initialized
14
+ * @returns A data object with schema-defined fields initialized
8
15
  */
9
16
  export declare function initFormDataFromSchema(schema: Record<string, unknown>, seed?: Record<string, unknown>): Record<string, unknown>;
10
17
  //# sourceMappingURL=initFormData.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"initFormData.d.ts","sourceRoot":"","sources":["../../src/core/initFormData.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyBzB"}
1
+ {"version":3,"file":"initFormData.d.ts","sourceRoot":"","sources":["../../src/core/initFormData.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB"}
@@ -1,18 +1,19 @@
1
1
  function initFormDataFromSchema(schema, seed) {
2
- const result = initProperty(schema, schema, seed);
3
- if (result && typeof result === "object" && !Array.isArray(result) && seed && typeof seed === "object") {
2
+ const result = initProperty(schema, schema, seed, true);
3
+ const base = result && typeof result === "object" && !Array.isArray(result) ? result : {};
4
+ if (seed && typeof seed === "object") {
4
5
  const schemaKeys = new Set(
5
6
  Object.keys(
6
7
  resolveRef(schema, schema)?.properties ?? {}
7
8
  )
8
9
  );
9
10
  for (const key of Object.keys(seed)) {
10
- if (!schemaKeys.has(key) && !(key in result)) {
11
- result[key] = seed[key];
11
+ if (!schemaKeys.has(key) && !(key in base)) {
12
+ base[key] = seed[key];
12
13
  }
13
14
  }
14
15
  }
15
- return result ?? {};
16
+ return base;
16
17
  }
17
18
  function resolveRef(property, root, seen) {
18
19
  if (!property || typeof property !== "object") return property;
@@ -38,14 +39,14 @@ function resolvePointer(obj, pointer) {
38
39
  }
39
40
  return current;
40
41
  }
41
- function initProperty(property, root, seed) {
42
- if (!property || typeof property !== "object") return null;
43
- const resolved = resolveRef(property, root);
44
- if (Array.isArray(resolved.oneOf)) {
45
- return initOneOf(resolved, root, seed);
42
+ function initProperty(property, root, seed, required) {
43
+ if (!property || typeof property !== "object") {
44
+ return required ? null : void 0;
46
45
  }
46
+ const resolved = resolveRef(property, root);
47
47
  const type = resolved.type;
48
- if (seed !== void 0 && seed !== null && type !== "object") {
48
+ const isOneOf = Array.isArray(resolved.oneOf);
49
+ if (seed !== void 0 && seed !== null && type !== "object" && !isOneOf) {
49
50
  return seed;
50
51
  }
51
52
  if ("const" in resolved) {
@@ -57,9 +58,25 @@ function initProperty(property, root, seed) {
57
58
  if ("default" in resolved) {
58
59
  return resolved.default;
59
60
  }
61
+ if (isOneOf) {
62
+ const value = initOneOf(resolved, root, seed, required);
63
+ if (!required && (value === void 0 || value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)) {
64
+ return void 0;
65
+ }
66
+ return value;
67
+ }
68
+ if (type === "object") {
69
+ const obj = initObject(
70
+ resolved,
71
+ root,
72
+ seed,
73
+ required
74
+ );
75
+ if (!required && Object.keys(obj).length === 0) return void 0;
76
+ return obj;
77
+ }
78
+ if (!required) return void 0;
60
79
  switch (type) {
61
- case "object":
62
- return initObject(resolved, root, seed);
63
80
  case "array":
64
81
  return seed !== void 0 && seed !== null ? seed : [];
65
82
  case "string":
@@ -68,30 +85,36 @@ function initProperty(property, root, seed) {
68
85
  return false;
69
86
  case "number":
70
87
  case "integer":
71
- return null;
72
88
  default:
73
89
  return null;
74
90
  }
75
91
  }
76
- function initObject(schema, root, seed) {
92
+ function initObject(schema, root, seed, parentRequired) {
77
93
  const properties = schema.properties;
78
94
  if (!properties) {
79
95
  return seed && typeof seed === "object" ? { ...seed } : {};
80
96
  }
97
+ const requiredSet = new Set(
98
+ Array.isArray(schema.required) ? schema.required : []
99
+ );
81
100
  const result = {};
82
101
  for (const [key, propSchema] of Object.entries(properties)) {
83
102
  const seedValue = seed && typeof seed === "object" ? seed[key] : void 0;
84
- result[key] = initProperty(propSchema, root, seedValue);
103
+ const effectiveRequired = parentRequired && requiredSet.has(key);
104
+ const value = initProperty(propSchema, root, seedValue, effectiveRequired);
105
+ if (value !== void 0) {
106
+ result[key] = value;
107
+ }
85
108
  }
86
109
  return result;
87
110
  }
88
- function initOneOf(schema, root, seed) {
111
+ function initOneOf(schema, root, seed, required) {
89
112
  const variants = schema.oneOf;
90
- if (!variants || variants.length === 0) return null;
113
+ if (!variants || variants.length === 0) return required ? null : void 0;
91
114
  const first = variants[0];
92
- if (!first) return null;
115
+ if (!first) return required ? null : void 0;
93
116
  const firstVariant = resolveRef(first, root);
94
- return initProperty(firstVariant, root, seed);
117
+ return initProperty(firstVariant, root, seed, required);
95
118
  }
96
119
  export {
97
120
  initFormDataFromSchema
@@ -1 +1 @@
1
- {"version":3,"file":"initFormData.js","sources":["../../src/core/initFormData.ts"],"sourcesContent":["/**\n * Initialize a form data object from a JSON Schema.\n * Resolves $ref, const, default, oneOf/discriminator, and typed empty values.\n *\n * @param schema - The full JSON Schema (must include $defs if $refs are used)\n * @param seed - Optional existing data to merge (seed values take priority)\n * @returns A data object with all schema-defined fields initialized\n */\nexport function initFormDataFromSchema(\n schema: Record<string, unknown>,\n seed?: Record<string, unknown>,\n): Record<string, unknown> {\n const result = initProperty(schema, schema, seed) as Record<string, unknown>;\n\n // If result is an object and seed has extra keys not in schema, preserve them\n if (\n result &&\n typeof result === \"object\" &&\n !Array.isArray(result) &&\n seed &&\n typeof seed === \"object\"\n ) {\n const schemaKeys = new Set(\n Object.keys(\n (resolveRef(schema, schema) as Record<string, unknown>)?.properties ??\n {},\n ),\n );\n for (const key of Object.keys(seed)) {\n if (!schemaKeys.has(key) && !(key in result)) {\n result[key] = seed[key];\n }\n }\n }\n\n return result ?? {};\n}\n\n/**\n * Resolve a $ref pointer against the root schema's $defs.\n * Supports nested $ref chains.\n */\nfunction resolveRef(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seen?: Set<string>,\n): Record<string, unknown> {\n if (!property || typeof property !== \"object\") return property;\n\n const ref = property.$ref as string | undefined;\n if (!ref) return property;\n\n // Guard against circular refs\n const visited = seen ?? new Set<string>();\n if (visited.has(ref)) return property;\n visited.add(ref);\n\n const resolved = resolvePointer(root, ref);\n if (!resolved) return property;\n\n // Continue resolving if the result itself has a $ref\n return resolveRef(resolved as Record<string, unknown>, root, visited);\n}\n\n/**\n * Resolve a JSON pointer like \"#/$defs/Price\" against an object.\n */\nfunction resolvePointer(\n obj: Record<string, unknown>,\n pointer: string,\n): unknown {\n if (!pointer.startsWith(\"#/\")) return undefined;\n const parts = pointer.slice(2).split(\"/\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current && typeof current === \"object\" && part in current) {\n current = (current as Record<string, unknown>)[part];\n } else {\n return undefined;\n }\n }\n return current;\n}\n\n/**\n * Initialize a single property value based on its schema definition.\n */\nfunction initProperty(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seed?: unknown,\n): unknown {\n if (!property || typeof property !== \"object\") return null;\n\n // Resolve $ref first\n const resolved = resolveRef(property, root);\n\n // Handle oneOf with discriminator — pick first variant\n if (Array.isArray(resolved.oneOf)) {\n return initOneOf(resolved, root, seed);\n }\n\n // Priority 1: seed wins (for object types, we merge recursively below)\n // For non-object types, return seed directly if present\n const type = resolved.type as string | undefined;\n if (seed !== undefined && seed !== null && type !== \"object\") {\n return seed;\n }\n\n // Priority 2: const\n if (\"const\" in resolved) {\n return resolved.const;\n }\n\n // Priority 3: single-value enum\n if (\n Array.isArray(resolved.enum) &&\n (resolved.enum as unknown[]).length === 1\n ) {\n return (resolved.enum as unknown[])[0];\n }\n\n // Priority 4: default\n if (\"default\" in resolved) {\n return resolved.default;\n }\n\n // Priority 5: typed empty values\n switch (type) {\n case \"object\":\n return initObject(resolved, root, seed as Record<string, unknown>);\n case \"array\":\n return seed !== undefined && seed !== null ? seed : [];\n case \"string\":\n return \"\";\n case \"boolean\":\n return false;\n case \"number\":\n case \"integer\":\n return null;\n default:\n return null;\n }\n}\n\n/**\n * Initialize an object type by recursing into its properties.\n */\nfunction initObject(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed?: Record<string, unknown>,\n): Record<string, unknown> {\n const properties = schema.properties as\n | Record<string, Record<string, unknown>>\n | undefined;\n if (!properties) {\n // Object with no defined properties — return seed or empty object\n return seed && typeof seed === \"object\" ? { ...seed } : {};\n }\n\n const result: Record<string, unknown> = {};\n\n for (const [key, propSchema] of Object.entries(properties)) {\n const seedValue = seed && typeof seed === \"object\" ? seed[key] : undefined;\n result[key] = initProperty(propSchema, root, seedValue);\n }\n\n return result;\n}\n\n/**\n * Handle oneOf schemas — pick the first variant and initialize it.\n */\nfunction initOneOf(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed?: unknown,\n): unknown {\n const variants = schema.oneOf as Record<string, unknown>[];\n if (!variants || variants.length === 0) return null;\n\n const first = variants[0];\n if (!first) return null;\n\n // Pick first variant, resolve its $ref if needed\n const firstVariant = resolveRef(first, root);\n return initProperty(firstVariant, root, seed);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"initFormData.js","sources":["../../src/core/initFormData.ts"],"sourcesContent":["/**\n * Initialize a form data object from a JSON Schema.\n * Resolves $ref, const, default, oneOf/discriminator, and typed empty values.\n *\n * Optional fields (not listed in the parent schema's `required` array) that\n * have no `const`, no single-value `enum`, and no `default` are omitted\n * entirely from the result. This avoids seeding values (e.g. `null` for\n * `type: \"integer\"`) that fail AJV's type check and surface spurious errors\n * on untouched fields. Required fields retain legacy typed-empty seeding\n * (`\"\"`, `null`, `false`, `[]`) so that \"is required\" surfaces cleanly.\n *\n * @param schema - The full JSON Schema (must include $defs if $refs are used)\n * @param seed - Optional existing data to merge (seed values take priority)\n * @returns A data object with schema-defined fields initialized\n */\nexport function initFormDataFromSchema(\n schema: Record<string, unknown>,\n seed?: Record<string, unknown>,\n): Record<string, unknown> {\n const result = initProperty(schema, schema, seed, true) as\n | Record<string, unknown>\n | undefined;\n\n const base =\n result && typeof result === \"object\" && !Array.isArray(result)\n ? result\n : {};\n\n // Preserve seed keys not described by the schema's properties.\n if (seed && typeof seed === \"object\") {\n const schemaKeys = new Set(\n Object.keys(\n (resolveRef(schema, schema) as Record<string, unknown>)?.properties ??\n {},\n ),\n );\n for (const key of Object.keys(seed)) {\n if (!schemaKeys.has(key) && !(key in base)) {\n base[key] = seed[key];\n }\n }\n }\n\n return base;\n}\n\n/**\n * Resolve a $ref pointer against the root schema's $defs.\n * Supports nested $ref chains.\n */\nfunction resolveRef(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seen?: Set<string>,\n): Record<string, unknown> {\n if (!property || typeof property !== \"object\") return property;\n\n const ref = property.$ref as string | undefined;\n if (!ref) return property;\n\n const visited = seen ?? new Set<string>();\n if (visited.has(ref)) return property;\n visited.add(ref);\n\n const resolved = resolvePointer(root, ref);\n if (!resolved) return property;\n\n return resolveRef(resolved as Record<string, unknown>, root, visited);\n}\n\n/**\n * Resolve a JSON pointer like \"#/$defs/Price\" against an object.\n */\nfunction resolvePointer(\n obj: Record<string, unknown>,\n pointer: string,\n): unknown {\n if (!pointer.startsWith(\"#/\")) return undefined;\n const parts = pointer.slice(2).split(\"/\");\n let current: unknown = obj;\n for (const part of parts) {\n if (current && typeof current === \"object\" && part in current) {\n current = (current as Record<string, unknown>)[part];\n } else {\n return undefined;\n }\n }\n return current;\n}\n\n/**\n * Initialize a single property value based on its schema definition.\n * Returns `undefined` when the property is optional and has nothing\n * concrete to seed — the caller then omits the key from its result.\n */\nfunction initProperty(\n property: Record<string, unknown>,\n root: Record<string, unknown>,\n seed: unknown,\n required: boolean,\n): unknown {\n if (!property || typeof property !== \"object\") {\n return required ? null : undefined;\n }\n\n const resolved = resolveRef(property, root);\n const type = resolved.type as string | undefined;\n const isOneOf = Array.isArray(resolved.oneOf);\n\n // Priority 1: seed wins for non-object, non-oneOf types. For oneOf we still\n // descend so that a partial seed (e.g. `{value: 5}` missing the\n // discriminator) picks up the variant's const/default for untouched fields.\n if (seed !== undefined && seed !== null && type !== \"object\" && !isOneOf) {\n return seed;\n }\n\n // Priority 2: const (schema-invariant — always set).\n if (\"const\" in resolved) {\n return resolved.const;\n }\n\n // Priority 3: single-value enum (same reasoning — only one valid value).\n if (\n Array.isArray(resolved.enum) &&\n (resolved.enum as unknown[]).length === 1\n ) {\n return (resolved.enum as unknown[])[0];\n }\n\n // Priority 4: default (explicit author intent — always honored).\n if (\"default\" in resolved) {\n return resolved.default;\n }\n\n // Priority 5: oneOf. Recurse into the first variant, propagating the\n // `required` flag so that the variant's own schema-declared required fields\n // only get typed-empty seeding when the container is required. For optional\n // containers, the variant's const / default / single-value enum still seed\n // (author intent is unconditional) but typed-empty placeholders do not.\n // If recursion yields nothing forced, we collapse back to undefined.\n if (isOneOf) {\n const value = initOneOf(resolved, root, seed, required);\n if (\n !required &&\n (value === undefined ||\n (value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.keys(value as Record<string, unknown>).length === 0))\n ) {\n return undefined;\n }\n return value;\n }\n\n // Priority 6: objects recurse. The `required` flag propagates downward:\n // inside an optional ancestor, nested required primitives also collapse,\n // so an optional object with nested required fields stays absent instead\n // of materializing a shell that AJV would flag.\n if (type === \"object\") {\n const obj = initObject(\n resolved,\n root,\n seed as Record<string, unknown>,\n required,\n );\n if (!required && Object.keys(obj).length === 0) return undefined;\n return obj;\n }\n\n // Priority 7: optional primitives — omit.\n if (!required) return undefined;\n\n // Priority 8: required primitives — legacy typed empty.\n switch (type) {\n case \"array\":\n return seed !== undefined && seed !== null ? seed : [];\n case \"string\":\n return \"\";\n case \"boolean\":\n return false;\n case \"number\":\n case \"integer\":\n default:\n return null;\n }\n}\n\n/**\n * Initialize an object type by recursing into its properties.\n * Keys whose initialization returns `undefined` are omitted.\n *\n * `parentRequired` controls how schema.required propagates: a child is\n * treated as required only if both its parent is required AND it appears in\n * the parent's required array. Inside an optional ancestor the whole\n * subtree collapses (except for author-forced values: const, default,\n * single-value enum, or seeded values).\n */\nfunction initObject(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed: Record<string, unknown> | undefined,\n parentRequired: boolean,\n): Record<string, unknown> {\n const properties = schema.properties as\n | Record<string, Record<string, unknown>>\n | undefined;\n if (!properties) {\n return seed && typeof seed === \"object\" ? { ...seed } : {};\n }\n\n const requiredSet = new Set<string>(\n Array.isArray(schema.required) ? (schema.required as string[]) : [],\n );\n\n const result: Record<string, unknown> = {};\n\n for (const [key, propSchema] of Object.entries(properties)) {\n const seedValue = seed && typeof seed === \"object\" ? seed[key] : undefined;\n const effectiveRequired = parentRequired && requiredSet.has(key);\n const value = initProperty(propSchema, root, seedValue, effectiveRequired);\n if (value !== undefined) {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n/**\n * Handle oneOf schemas — pick the first variant and initialize it.\n * `required` propagates: if the outer oneOf is required, the variant is\n * materialized with typed-empty seeding for its required fields; if optional,\n * only author-forced values (const / default / single-enum / seed) survive.\n */\nfunction initOneOf(\n schema: Record<string, unknown>,\n root: Record<string, unknown>,\n seed: unknown,\n required: boolean,\n): unknown {\n const variants = schema.oneOf as Record<string, unknown>[];\n if (!variants || variants.length === 0) return required ? null : undefined;\n\n const first = variants[0];\n if (!first) return required ? null : undefined;\n\n const firstVariant = resolveRef(first, root);\n return initProperty(firstVariant, root, seed, required);\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narrative.io/jsonforms-provider-protocols",
3
- "version": "3.0.0-beta.12",
3
+ "version": "3.0.0-beta.14",
4
4
  "description": "Dynamic data provider capabilities for JSONForms with Vue 3 integration",
5
5
  "type": "module",
6
6
  "author": "Narrative I/O",
@@ -2,24 +2,32 @@
2
2
  * Initialize a form data object from a JSON Schema.
3
3
  * Resolves $ref, const, default, oneOf/discriminator, and typed empty values.
4
4
  *
5
+ * Optional fields (not listed in the parent schema's `required` array) that
6
+ * have no `const`, no single-value `enum`, and no `default` are omitted
7
+ * entirely from the result. This avoids seeding values (e.g. `null` for
8
+ * `type: "integer"`) that fail AJV's type check and surface spurious errors
9
+ * on untouched fields. Required fields retain legacy typed-empty seeding
10
+ * (`""`, `null`, `false`, `[]`) so that "is required" surfaces cleanly.
11
+ *
5
12
  * @param schema - The full JSON Schema (must include $defs if $refs are used)
6
13
  * @param seed - Optional existing data to merge (seed values take priority)
7
- * @returns A data object with all schema-defined fields initialized
14
+ * @returns A data object with schema-defined fields initialized
8
15
  */
9
16
  export function initFormDataFromSchema(
10
17
  schema: Record<string, unknown>,
11
18
  seed?: Record<string, unknown>,
12
19
  ): Record<string, unknown> {
13
- const result = initProperty(schema, schema, seed) as Record<string, unknown>;
20
+ const result = initProperty(schema, schema, seed, true) as
21
+ | Record<string, unknown>
22
+ | undefined;
14
23
 
15
- // If result is an object and seed has extra keys not in schema, preserve them
16
- if (
17
- result &&
18
- typeof result === "object" &&
19
- !Array.isArray(result) &&
20
- seed &&
21
- typeof seed === "object"
22
- ) {
24
+ const base =
25
+ result && typeof result === "object" && !Array.isArray(result)
26
+ ? result
27
+ : {};
28
+
29
+ // Preserve seed keys not described by the schema's properties.
30
+ if (seed && typeof seed === "object") {
23
31
  const schemaKeys = new Set(
24
32
  Object.keys(
25
33
  (resolveRef(schema, schema) as Record<string, unknown>)?.properties ??
@@ -27,13 +35,13 @@ export function initFormDataFromSchema(
27
35
  ),
28
36
  );
29
37
  for (const key of Object.keys(seed)) {
30
- if (!schemaKeys.has(key) && !(key in result)) {
31
- result[key] = seed[key];
38
+ if (!schemaKeys.has(key) && !(key in base)) {
39
+ base[key] = seed[key];
32
40
  }
33
41
  }
34
42
  }
35
43
 
36
- return result ?? {};
44
+ return base;
37
45
  }
38
46
 
39
47
  /**
@@ -50,7 +58,6 @@ function resolveRef(
50
58
  const ref = property.$ref as string | undefined;
51
59
  if (!ref) return property;
52
60
 
53
- // Guard against circular refs
54
61
  const visited = seen ?? new Set<string>();
55
62
  if (visited.has(ref)) return property;
56
63
  visited.add(ref);
@@ -58,7 +65,6 @@ function resolveRef(
58
65
  const resolved = resolvePointer(root, ref);
59
66
  if (!resolved) return property;
60
67
 
61
- // Continue resolving if the result itself has a $ref
62
68
  return resolveRef(resolved as Record<string, unknown>, root, visited);
63
69
  }
64
70
 
@@ -84,35 +90,36 @@ function resolvePointer(
84
90
 
85
91
  /**
86
92
  * Initialize a single property value based on its schema definition.
93
+ * Returns `undefined` when the property is optional and has nothing
94
+ * concrete to seed — the caller then omits the key from its result.
87
95
  */
88
96
  function initProperty(
89
97
  property: Record<string, unknown>,
90
98
  root: Record<string, unknown>,
91
- seed?: unknown,
99
+ seed: unknown,
100
+ required: boolean,
92
101
  ): unknown {
93
- if (!property || typeof property !== "object") return null;
94
-
95
- // Resolve $ref first
96
- const resolved = resolveRef(property, root);
97
-
98
- // Handle oneOf with discriminator — pick first variant
99
- if (Array.isArray(resolved.oneOf)) {
100
- return initOneOf(resolved, root, seed);
102
+ if (!property || typeof property !== "object") {
103
+ return required ? null : undefined;
101
104
  }
102
105
 
103
- // Priority 1: seed wins (for object types, we merge recursively below)
104
- // For non-object types, return seed directly if present
106
+ const resolved = resolveRef(property, root);
105
107
  const type = resolved.type as string | undefined;
106
- if (seed !== undefined && seed !== null && type !== "object") {
108
+ const isOneOf = Array.isArray(resolved.oneOf);
109
+
110
+ // Priority 1: seed wins for non-object, non-oneOf types. For oneOf we still
111
+ // descend so that a partial seed (e.g. `{value: 5}` missing the
112
+ // discriminator) picks up the variant's const/default for untouched fields.
113
+ if (seed !== undefined && seed !== null && type !== "object" && !isOneOf) {
107
114
  return seed;
108
115
  }
109
116
 
110
- // Priority 2: const
117
+ // Priority 2: const (schema-invariant — always set).
111
118
  if ("const" in resolved) {
112
119
  return resolved.const;
113
120
  }
114
121
 
115
- // Priority 3: single-value enum
122
+ // Priority 3: single-value enum (same reasoning — only one valid value).
116
123
  if (
117
124
  Array.isArray(resolved.enum) &&
118
125
  (resolved.enum as unknown[]).length === 1
@@ -120,15 +127,52 @@ function initProperty(
120
127
  return (resolved.enum as unknown[])[0];
121
128
  }
122
129
 
123
- // Priority 4: default
130
+ // Priority 4: default (explicit author intent — always honored).
124
131
  if ("default" in resolved) {
125
132
  return resolved.default;
126
133
  }
127
134
 
128
- // Priority 5: typed empty values
135
+ // Priority 5: oneOf. Recurse into the first variant, propagating the
136
+ // `required` flag so that the variant's own schema-declared required fields
137
+ // only get typed-empty seeding when the container is required. For optional
138
+ // containers, the variant's const / default / single-value enum still seed
139
+ // (author intent is unconditional) but typed-empty placeholders do not.
140
+ // If recursion yields nothing forced, we collapse back to undefined.
141
+ if (isOneOf) {
142
+ const value = initOneOf(resolved, root, seed, required);
143
+ if (
144
+ !required &&
145
+ (value === undefined ||
146
+ (value !== null &&
147
+ typeof value === "object" &&
148
+ !Array.isArray(value) &&
149
+ Object.keys(value as Record<string, unknown>).length === 0))
150
+ ) {
151
+ return undefined;
152
+ }
153
+ return value;
154
+ }
155
+
156
+ // Priority 6: objects recurse. The `required` flag propagates downward:
157
+ // inside an optional ancestor, nested required primitives also collapse,
158
+ // so an optional object with nested required fields stays absent instead
159
+ // of materializing a shell that AJV would flag.
160
+ if (type === "object") {
161
+ const obj = initObject(
162
+ resolved,
163
+ root,
164
+ seed as Record<string, unknown>,
165
+ required,
166
+ );
167
+ if (!required && Object.keys(obj).length === 0) return undefined;
168
+ return obj;
169
+ }
170
+
171
+ // Priority 7: optional primitives — omit.
172
+ if (!required) return undefined;
173
+
174
+ // Priority 8: required primitives — legacy typed empty.
129
175
  switch (type) {
130
- case "object":
131
- return initObject(resolved, root, seed as Record<string, unknown>);
132
176
  case "array":
133
177
  return seed !== undefined && seed !== null ? seed : [];
134
178
  case "string":
@@ -137,7 +181,6 @@ function initProperty(
137
181
  return false;
138
182
  case "number":
139
183
  case "integer":
140
- return null;
141
184
  default:
142
185
  return null;
143
186
  }
@@ -145,25 +188,40 @@ function initProperty(
145
188
 
146
189
  /**
147
190
  * Initialize an object type by recursing into its properties.
191
+ * Keys whose initialization returns `undefined` are omitted.
192
+ *
193
+ * `parentRequired` controls how schema.required propagates: a child is
194
+ * treated as required only if both its parent is required AND it appears in
195
+ * the parent's required array. Inside an optional ancestor the whole
196
+ * subtree collapses (except for author-forced values: const, default,
197
+ * single-value enum, or seeded values).
148
198
  */
149
199
  function initObject(
150
200
  schema: Record<string, unknown>,
151
201
  root: Record<string, unknown>,
152
- seed?: Record<string, unknown>,
202
+ seed: Record<string, unknown> | undefined,
203
+ parentRequired: boolean,
153
204
  ): Record<string, unknown> {
154
205
  const properties = schema.properties as
155
206
  | Record<string, Record<string, unknown>>
156
207
  | undefined;
157
208
  if (!properties) {
158
- // Object with no defined properties — return seed or empty object
159
209
  return seed && typeof seed === "object" ? { ...seed } : {};
160
210
  }
161
211
 
212
+ const requiredSet = new Set<string>(
213
+ Array.isArray(schema.required) ? (schema.required as string[]) : [],
214
+ );
215
+
162
216
  const result: Record<string, unknown> = {};
163
217
 
164
218
  for (const [key, propSchema] of Object.entries(properties)) {
165
219
  const seedValue = seed && typeof seed === "object" ? seed[key] : undefined;
166
- result[key] = initProperty(propSchema, root, seedValue);
220
+ const effectiveRequired = parentRequired && requiredSet.has(key);
221
+ const value = initProperty(propSchema, root, seedValue, effectiveRequired);
222
+ if (value !== undefined) {
223
+ result[key] = value;
224
+ }
167
225
  }
168
226
 
169
227
  return result;
@@ -171,19 +229,22 @@ function initObject(
171
229
 
172
230
  /**
173
231
  * Handle oneOf schemas — pick the first variant and initialize it.
232
+ * `required` propagates: if the outer oneOf is required, the variant is
233
+ * materialized with typed-empty seeding for its required fields; if optional,
234
+ * only author-forced values (const / default / single-enum / seed) survive.
174
235
  */
175
236
  function initOneOf(
176
237
  schema: Record<string, unknown>,
177
238
  root: Record<string, unknown>,
178
- seed?: unknown,
239
+ seed: unknown,
240
+ required: boolean,
179
241
  ): unknown {
180
242
  const variants = schema.oneOf as Record<string, unknown>[];
181
- if (!variants || variants.length === 0) return null;
243
+ if (!variants || variants.length === 0) return required ? null : undefined;
182
244
 
183
245
  const first = variants[0];
184
- if (!first) return null;
246
+ if (!first) return required ? null : undefined;
185
247
 
186
- // Pick first variant, resolve its $ref if needed
187
248
  const firstVariant = resolveRef(first, root);
188
- return initProperty(firstVariant, root, seed);
249
+ return initProperty(firstVariant, root, seed, required);
189
250
  }