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

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,12 +39,11 @@ 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
48
  if (seed !== void 0 && seed !== null && type !== "object") {
49
49
  return seed;
@@ -57,9 +57,22 @@ function initProperty(property, root, seed) {
57
57
  if ("default" in resolved) {
58
58
  return resolved.default;
59
59
  }
60
+ if (Array.isArray(resolved.oneOf)) {
61
+ if (!required) return void 0;
62
+ return initOneOf(resolved, root, seed);
63
+ }
64
+ if (type === "object") {
65
+ const obj = initObject(
66
+ resolved,
67
+ root,
68
+ seed,
69
+ required
70
+ );
71
+ if (!required && Object.keys(obj).length === 0) return void 0;
72
+ return obj;
73
+ }
74
+ if (!required) return void 0;
60
75
  switch (type) {
61
- case "object":
62
- return initObject(resolved, root, seed);
63
76
  case "array":
64
77
  return seed !== void 0 && seed !== null ? seed : [];
65
78
  case "string":
@@ -68,20 +81,26 @@ function initProperty(property, root, seed) {
68
81
  return false;
69
82
  case "number":
70
83
  case "integer":
71
- return null;
72
84
  default:
73
85
  return null;
74
86
  }
75
87
  }
76
- function initObject(schema, root, seed) {
88
+ function initObject(schema, root, seed, parentRequired) {
77
89
  const properties = schema.properties;
78
90
  if (!properties) {
79
91
  return seed && typeof seed === "object" ? { ...seed } : {};
80
92
  }
93
+ const requiredSet = new Set(
94
+ Array.isArray(schema.required) ? schema.required : []
95
+ );
81
96
  const result = {};
82
97
  for (const [key, propSchema] of Object.entries(properties)) {
83
98
  const seedValue = seed && typeof seed === "object" ? seed[key] : void 0;
84
- result[key] = initProperty(propSchema, root, seedValue);
99
+ const effectiveRequired = parentRequired && requiredSet.has(key);
100
+ const value = initProperty(propSchema, root, seedValue, effectiveRequired);
101
+ if (value !== void 0) {
102
+ result[key] = value;
103
+ }
85
104
  }
86
105
  return result;
87
106
  }
@@ -91,7 +110,7 @@ function initOneOf(schema, root, seed) {
91
110
  const first = variants[0];
92
111
  if (!first) return null;
93
112
  const firstVariant = resolveRef(first, root);
94
- return initProperty(firstVariant, root, seed);
113
+ return initProperty(firstVariant, root, seed, true);
95
114
  }
96
115
  export {
97
116
  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\n // Priority 1: seed wins for non-object types.\n if (seed !== undefined && seed !== null && type !== \"object\") {\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. For required fields, materialize the first variant\n // so the discriminator and shape are present. For optional fields, leave\n // undefined so the user isn't forced into a variant they haven't chosen.\n if (Array.isArray(resolved.oneOf)) {\n if (!required) return undefined;\n return initOneOf(resolved, root, seed);\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 * The variant is treated as required so its own required discriminator\n * and structure seed cleanly.\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 const firstVariant = resolveRef(first, root);\n return initProperty(firstVariant, root, seed, true);\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.13",
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,33 @@ 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;
108
+
109
+ // Priority 1: seed wins for non-object types.
106
110
  if (seed !== undefined && seed !== null && type !== "object") {
107
111
  return seed;
108
112
  }
109
113
 
110
- // Priority 2: const
114
+ // Priority 2: const (schema-invariant — always set).
111
115
  if ("const" in resolved) {
112
116
  return resolved.const;
113
117
  }
114
118
 
115
- // Priority 3: single-value enum
119
+ // Priority 3: single-value enum (same reasoning — only one valid value).
116
120
  if (
117
121
  Array.isArray(resolved.enum) &&
118
122
  (resolved.enum as unknown[]).length === 1
@@ -120,15 +124,39 @@ function initProperty(
120
124
  return (resolved.enum as unknown[])[0];
121
125
  }
122
126
 
123
- // Priority 4: default
127
+ // Priority 4: default (explicit author intent — always honored).
124
128
  if ("default" in resolved) {
125
129
  return resolved.default;
126
130
  }
127
131
 
128
- // Priority 5: typed empty values
132
+ // Priority 5: oneOf. For required fields, materialize the first variant
133
+ // so the discriminator and shape are present. For optional fields, leave
134
+ // undefined so the user isn't forced into a variant they haven't chosen.
135
+ if (Array.isArray(resolved.oneOf)) {
136
+ if (!required) return undefined;
137
+ return initOneOf(resolved, root, seed);
138
+ }
139
+
140
+ // Priority 6: objects recurse. The `required` flag propagates downward:
141
+ // inside an optional ancestor, nested required primitives also collapse,
142
+ // so an optional object with nested required fields stays absent instead
143
+ // of materializing a shell that AJV would flag.
144
+ if (type === "object") {
145
+ const obj = initObject(
146
+ resolved,
147
+ root,
148
+ seed as Record<string, unknown>,
149
+ required,
150
+ );
151
+ if (!required && Object.keys(obj).length === 0) return undefined;
152
+ return obj;
153
+ }
154
+
155
+ // Priority 7: optional primitives — omit.
156
+ if (!required) return undefined;
157
+
158
+ // Priority 8: required primitives — legacy typed empty.
129
159
  switch (type) {
130
- case "object":
131
- return initObject(resolved, root, seed as Record<string, unknown>);
132
160
  case "array":
133
161
  return seed !== undefined && seed !== null ? seed : [];
134
162
  case "string":
@@ -137,7 +165,6 @@ function initProperty(
137
165
  return false;
138
166
  case "number":
139
167
  case "integer":
140
- return null;
141
168
  default:
142
169
  return null;
143
170
  }
@@ -145,25 +172,40 @@ function initProperty(
145
172
 
146
173
  /**
147
174
  * Initialize an object type by recursing into its properties.
175
+ * Keys whose initialization returns `undefined` are omitted.
176
+ *
177
+ * `parentRequired` controls how schema.required propagates: a child is
178
+ * treated as required only if both its parent is required AND it appears in
179
+ * the parent's required array. Inside an optional ancestor the whole
180
+ * subtree collapses (except for author-forced values: const, default,
181
+ * single-value enum, or seeded values).
148
182
  */
149
183
  function initObject(
150
184
  schema: Record<string, unknown>,
151
185
  root: Record<string, unknown>,
152
- seed?: Record<string, unknown>,
186
+ seed: Record<string, unknown> | undefined,
187
+ parentRequired: boolean,
153
188
  ): Record<string, unknown> {
154
189
  const properties = schema.properties as
155
190
  | Record<string, Record<string, unknown>>
156
191
  | undefined;
157
192
  if (!properties) {
158
- // Object with no defined properties — return seed or empty object
159
193
  return seed && typeof seed === "object" ? { ...seed } : {};
160
194
  }
161
195
 
196
+ const requiredSet = new Set<string>(
197
+ Array.isArray(schema.required) ? (schema.required as string[]) : [],
198
+ );
199
+
162
200
  const result: Record<string, unknown> = {};
163
201
 
164
202
  for (const [key, propSchema] of Object.entries(properties)) {
165
203
  const seedValue = seed && typeof seed === "object" ? seed[key] : undefined;
166
- result[key] = initProperty(propSchema, root, seedValue);
204
+ const effectiveRequired = parentRequired && requiredSet.has(key);
205
+ const value = initProperty(propSchema, root, seedValue, effectiveRequired);
206
+ if (value !== undefined) {
207
+ result[key] = value;
208
+ }
167
209
  }
168
210
 
169
211
  return result;
@@ -171,6 +213,8 @@ function initObject(
171
213
 
172
214
  /**
173
215
  * Handle oneOf schemas — pick the first variant and initialize it.
216
+ * The variant is treated as required so its own required discriminator
217
+ * and structure seed cleanly.
174
218
  */
175
219
  function initOneOf(
176
220
  schema: Record<string, unknown>,
@@ -183,7 +227,6 @@ function initOneOf(
183
227
  const first = variants[0];
184
228
  if (!first) return null;
185
229
 
186
- // Pick first variant, resolve its $ref if needed
187
230
  const firstVariant = resolveRef(first, root);
188
- return initProperty(firstVariant, root, seed);
231
+ return initProperty(firstVariant, root, seed, true);
189
232
  }