zopia 0.3.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +354 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -0
  4. package/bin/zopia.js +20 -0
  5. package/docs/01-overview.md +94 -0
  6. package/docs/02-targets.md +55 -0
  7. package/docs/03-roadmap.md +205 -0
  8. package/docs/04-architecture.md +345 -0
  9. package/docs/05-concepts.md +239 -0
  10. package/docs/06-conversions.md +493 -0
  11. package/docs/07-api-docs.md +337 -0
  12. package/docs/08-components.md +223 -0
  13. package/docs/09-configuration.md +167 -0
  14. package/docs/10-usage.md +208 -0
  15. package/docs/11-testing.md +267 -0
  16. package/docs/12-standards.md +242 -0
  17. package/docs/README.md +42 -0
  18. package/docs/publish-workflow.yml.example +48 -0
  19. package/package.json +77 -0
  20. package/src/api-docs-navigation.ts +353 -0
  21. package/src/cli-command.ts +537 -0
  22. package/src/cli.ts +4 -0
  23. package/src/config.ts +190 -0
  24. package/src/conversions/api-docs-facade.ts +42 -0
  25. package/src/conversions/api-docs-generate.ts +567 -0
  26. package/src/conversions/api-docs-layout.ts +39 -0
  27. package/src/conversions/api-docs-plan.ts +130 -0
  28. package/src/conversions/api-docs-presets.ts +246 -0
  29. package/src/conversions/json-schema-to-zod.ts +931 -0
  30. package/src/conversions/manifest-staleness.ts +211 -0
  31. package/src/conversions/manifest-to-openapi.ts +1861 -0
  32. package/src/conversions/manifest-writer.ts +778 -0
  33. package/src/conversions/openapi-contracts.ts +333 -0
  34. package/src/conversions/openapi-external-ref.ts +233 -0
  35. package/src/conversions/openapi-ir.ts +74 -0
  36. package/src/conversions/openapi-ref.ts +38 -0
  37. package/src/conversions/openapi-to-api-docs-public.ts +466 -0
  38. package/src/conversions/openapi-to-api-docs.ts +203 -0
  39. package/src/conversions/openapi.ts +80 -0
  40. package/src/conversions/reverse-security.ts +68 -0
  41. package/src/conversions/yaml.ts +876 -0
  42. package/src/conversions/zod-to-json-schema.ts +536 -0
  43. package/src/diff.ts +353 -0
  44. package/src/errors.ts +114 -0
  45. package/src/index.ts +80 -0
  46. package/src/validation.ts +299 -0
  47. package/src/warnings.ts +164 -0
@@ -0,0 +1,931 @@
1
+ import { asZopiaError, ZopiaError } from '../errors';
2
+ import { readFileSync } from 'node:fs';
3
+ import { z } from 'zod';
4
+ import { formatZopiaWarningComment, normalizeZopiaWarnings, type ZopiaWarning, type ZopiaWarningCode } from '../warnings';
5
+ import { decodeJsonPointerSegment } from './openapi-ref';
6
+
7
+ /** A JSON Schema object or boolean schema. */
8
+ export type JsonSchema = Record<string, any> | boolean;
9
+
10
+ /** A manifest-compatible restoration for schema facts that Zod cannot serialize exactly. */
11
+ export interface JsonSchemaOverlay {
12
+ /** RFC 6901 pointer relative to the converted schema; the empty string addresses its root. */
13
+ at: string;
14
+ /** Keywords to restore on the runtime-generated schema object. */
15
+ set?: Record<string, unknown>;
16
+ /** Runtime-generated keywords to remove before applying `set`. */
17
+ remove?: string[];
18
+ /** Original subtree to restore verbatim when code edits cannot safely propagate. */
19
+ node?: JsonSchema;
20
+ }
21
+
22
+ /** Options controlling JSON-Schema-to-Zod conversion. */
23
+ export interface JsonSchemaToZodOptions {
24
+ /** Identifier used by the emitted root declaration. @default 'schema' */
25
+ rootName?: string;
26
+ }
27
+
28
+ /** Executable Zod output plus every non-fatal diagnostic and round-trip restoration. */
29
+ export interface JsonSchemaToZodResult {
30
+ /** TypeScript source containing local definitions and the named root declaration. */
31
+ code: string;
32
+ /** Runtime schema equivalent to `code`. */
33
+ schema: z.ZodType;
34
+ /** Structured warnings for malformed, approximated, or frozen source constructs. */
35
+ warnings: ZopiaWarning[];
36
+ /** Manifest-compatible restorations for non-identity mappings. */
37
+ overlays: JsonSchemaOverlay[];
38
+ }
39
+
40
+ interface AnalysisWarning extends ZopiaWarning { keyword: string; }
41
+
42
+ const pointer = (at: string, token: string | number): string => `${at}/${String(token).replace(/~/g, '~0').replace(/\//g, '~1')}`;
43
+ const warningAt = (at: string): string => at === '' ? '#' : `#${at}`;
44
+ const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
45
+ const sortedEntries = <T>(value: Record<string, T>): Array<[string, T]> => Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
46
+ const isSchemaValue = (value: unknown): value is JsonSchema => typeof value === 'boolean' || (value !== null && typeof value === 'object' && !Array.isArray(value));
47
+
48
+ const nativeRecordKeyConstraint = (value: unknown): boolean => {
49
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
50
+ const record = value as Record<string, unknown>;
51
+ if (record.type !== 'string') return false;
52
+ if (Object.keys(record).some((key) => !['type', 'pattern', 'minLength', 'maxLength'].includes(key))) return false;
53
+ if (record.pattern === undefined && record.minLength === undefined && record.maxLength === undefined) return false;
54
+ if (record.pattern !== undefined) {
55
+ if (typeof record.pattern !== 'string') return false;
56
+ try { new RegExp(record.pattern); } catch { return false; }
57
+ }
58
+ for (const bound of ['minLength', 'maxLength'] as const) if (record[bound] !== undefined && (typeof record[bound] !== 'number' || !Number.isSafeInteger(record[bound]) || (record[bound] as number) < 0)) return false;
59
+ return true;
60
+ };
61
+
62
+ /**
63
+ * Detect the exact `z.record(key, value)` ↔ schema form (D-22): an object node declaring
64
+ * only type/propertyNames/additionalProperties where propertyNames is an explicit string
65
+ * constraint and additionalProperties is an object schema. Zod emits the identical shape
66
+ * back, so no refinement approximation or frozen overlay is needed.
67
+ */
68
+ function isNativeRecordObject(node: unknown): boolean {
69
+ if (!isSchemaValue(node) || typeof node === 'boolean') return false;
70
+ const record = node as Record<string, unknown>;
71
+ if (record.type !== 'object') return false;
72
+ for (const key of ['properties', 'patternProperties', 'required', 'minProperties', 'maxProperties', 'dependencies', 'dependentRequired', 'dependentSchemas', 'unevaluatedProperties', 'unevaluatedItems']) {
73
+ if (Object.prototype.hasOwnProperty.call(record, key)) return false;
74
+ }
75
+ if (!nativeRecordKeyConstraint(record.propertyNames)) return false;
76
+ return record.additionalProperties !== null && typeof record.additionalProperties === 'object' && !Array.isArray(record.additionalProperties);
77
+ }
78
+
79
+ function analyzeSchema(source: JsonSchema): { warnings: AnalysisWarning[]; overlays: JsonSchemaOverlay[] } {
80
+ const warnings: AnalysisWarning[] = [];
81
+ const overlays: JsonSchemaOverlay[] = [];
82
+ const warn = (code: ZopiaWarningCode, at: string, keyword: string, message: string): void => {
83
+ warnings.push({ code, at: warningAt(at), keyword, message });
84
+ };
85
+ const addOverlay = (entry: JsonSchemaOverlay): void => {
86
+ const frozenParent = overlays.find((existing) => Object.prototype.hasOwnProperty.call(existing, 'node') && (existing.at === '' || entry.at === existing.at || entry.at.startsWith(`${existing.at}/`)));
87
+ if (frozenParent) return;
88
+ if (Object.prototype.hasOwnProperty.call(entry, 'node')) {
89
+ for (let index = overlays.length - 1; index >= 0; index -= 1) if (overlays[index].at === entry.at || overlays[index].at.startsWith(`${entry.at}/`)) overlays.splice(index, 1);
90
+ overlays.push(entry);
91
+ return;
92
+ }
93
+ const existing = overlays.find((candidate) => candidate.at === entry.at && !Object.prototype.hasOwnProperty.call(candidate, 'node'));
94
+ if (!existing) { overlays.push(entry); return; }
95
+ if (entry.set) existing.set = { ...(existing.set ?? {}), ...entry.set };
96
+ if (entry.remove) existing.remove = [...new Set([...(existing.remove ?? []), ...entry.remove])].filter((key) => !Object.prototype.hasOwnProperty.call(existing.set ?? {}, key));
97
+ };
98
+ const freeze = (node: Record<string, any>, at: string, code: ZopiaWarningCode, keyword: string, message: string): void => {
99
+ warn(code, at, keyword, message);
100
+ addOverlay({ at, node: cloneJson(node) });
101
+ };
102
+ const visit = (node: JsonSchema, at = '', _parentKeyword?: string): void => {
103
+ if (typeof node === 'boolean') { addOverlay({ at, node }); return; }
104
+
105
+ if (Object.prototype.hasOwnProperty.call(node, 'not')) {
106
+ const exactNever = Object.keys(node).length === 1 && node.not !== null && typeof node.not === 'object' && !Array.isArray(node.not) && Object.keys(node.not).length === 0;
107
+ if (!exactNever) freeze(node, at, 'ZOPIA_WARN_NOT', 'not', '`not` is enforced by a Zod refinement and frozen for exact reverse conversion');
108
+ return;
109
+ }
110
+ if (node.nullable === false) addOverlay({ at, set: { nullable: false } });
111
+ for (const keyword of ['exclusiveMinimum', 'exclusiveMaximum'] as const) if (node[keyword] === false) addOverlay({ at, set: { [keyword]: false } });
112
+ if (node.type === 'array' && node.items === false && !Array.isArray(node.prefixItems)) addOverlay({
113
+ at,
114
+ set: { items: false, ...(Object.prototype.hasOwnProperty.call(node, 'maxItems') ? { maxItems: cloneJson(node.maxItems) } : {}) },
115
+ remove: ['prefixItems', ...(Object.prototype.hasOwnProperty.call(node, 'maxItems') ? [] : ['maxItems'])],
116
+ });
117
+ if (Object.prototype.hasOwnProperty.call(node, 'allOf')) {
118
+ freeze(node, at, 'ZOPIA_WARN_FROZEN_SUBTREE', 'allOf', '`allOf` structure is frozen because Zod intersections can serialize with a different shape');
119
+ return;
120
+ }
121
+ if (['if', 'then', 'else'].some((key) => Object.prototype.hasOwnProperty.call(node, key))) {
122
+ freeze(node, at, 'ZOPIA_WARN_FROZEN_SUBTREE', 'if', 'conditional schema structure is frozen because Zod has no serializable conditional equivalent');
123
+ return;
124
+ }
125
+ const refinementKeyword = ['patternProperties', 'propertyNames', 'minProperties', 'maxProperties', 'contains', 'minContains', 'maxContains', 'dependencies', 'dependentRequired', 'dependentSchemas'].find((key) => Object.prototype.hasOwnProperty.call(node, key));
126
+ if (refinementKeyword && !(refinementKeyword === 'propertyNames' && isNativeRecordObject(node))) {
127
+ freeze(node, at, 'ZOPIA_WARN_FROZEN_SUBTREE', refinementKeyword, `\`${refinementKeyword}\` is implemented with runtime refinements and frozen for exact reverse conversion`);
128
+ return;
129
+ }
130
+
131
+ if (Array.isArray(node.enum) && node.type === undefined && node.enum.length > 0 && node.enum.every((value: unknown) => typeof value === 'string')) addOverlay({ at, remove: ['type'] });
132
+ if (Array.isArray(node.oneOf)) {
133
+ warn('ZOPIA_WARN_ONE_OF', at, 'oneOf', '`oneOf` requires an overlay because Zod unions do not serialize exclusivity and discriminator metadata identically');
134
+ addOverlay({ at, set: { oneOf: cloneJson(node.oneOf), ...(node.discriminator === undefined ? {} : { discriminator: cloneJson(node.discriminator) }) }, remove: ['anyOf'] });
135
+ }
136
+ if (node.uniqueItems === true) {
137
+ warn('ZOPIA_WARN_UNIQUE_ITEMS', at, 'uniqueItems', '`uniqueItems` is enforced by a refinement and restored by an overlay');
138
+ addOverlay({ at, set: { uniqueItems: true } });
139
+ }
140
+ if (node.type === 'array' && (Array.isArray(node.prefixItems) || Array.isArray(node.items))) {
141
+ const draftTuple = Array.isArray(node.items);
142
+ const set = {
143
+ ...Object.fromEntries(['minItems', 'maxItems'].filter((key) => Object.prototype.hasOwnProperty.call(node, key)).map((key) => [key, cloneJson(node[key])])),
144
+ ...(draftTuple ? {
145
+ items: cloneJson(node.items),
146
+ ...(Object.prototype.hasOwnProperty.call(node, 'additionalItems') ? { additionalItems: cloneJson(node.additionalItems) } : {}),
147
+ } : node.items === undefined && node.unevaluatedItems !== undefined ? { unevaluatedItems: cloneJson(node.unevaluatedItems) } : {}),
148
+ };
149
+ const closed = draftTuple
150
+ ? node.additionalItems === false
151
+ : node.items === false || node.items === undefined && node.unevaluatedItems === false;
152
+ const remove = [
153
+ ...(draftTuple ? ['prefixItems'] : node.items === undefined ? ['items'] : []),
154
+ ...(!Object.prototype.hasOwnProperty.call(node, 'maxItems') && closed ? ['maxItems'] : []),
155
+ ];
156
+ if (Object.keys(set).length || remove.length) addOverlay({ at, ...(Object.keys(set).length ? { set } : {}), ...(remove.length ? { remove } : {}) });
157
+ }
158
+ for (const [name, bound] of [['exclusiveMinimum', node.exclusiveMinimum], ['exclusiveMaximum', node.exclusiveMaximum]] as const) {
159
+ if (bound !== true) continue;
160
+ warn('ZOPIA_WARN_LEGACY_EXCLUSIVE_BOUND', at, name, `legacy boolean \`${name}\` is approximated by a Zod numeric bound`);
161
+ const inclusive = name === 'exclusiveMinimum' ? 'minimum' : 'maximum';
162
+ addOverlay({ at, set: { [name]: true, ...(Object.prototype.hasOwnProperty.call(node, inclusive) ? { [inclusive]: node[inclusive] } : {}) } });
163
+ }
164
+
165
+ if (typeof node.format === 'string') {
166
+ const exact = new Set(['email', 'uuid', 'hostname', 'ipv4', 'ipv6', 'date-time', 'date', 'duration', 'uri']);
167
+ const acceptsString = node.type === undefined || node.type === 'string' || (Array.isArray(node.type) && node.type.includes('string'));
168
+ const aliases: Record<string, { set: Record<string, unknown>; remove?: string[]; code?: ZopiaWarningCode }> = {
169
+ url: { set: { format: 'url' }, code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
170
+ time: { set: { format: 'time' }, remove: ['pattern'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
171
+ byte: { set: { format: 'byte' }, remove: ['pattern', 'contentEncoding'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
172
+ base64: { set: { format: 'base64' }, remove: ['pattern', 'contentEncoding'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
173
+ base64url: { set: { format: 'base64url' }, remove: ['pattern', 'contentEncoding'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
174
+ emoji: { set: { format: 'emoji' }, remove: ['pattern'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
175
+ int32: { set: { format: 'int32' }, remove: ['minimum', 'maximum'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
176
+ int64: { set: { format: 'int64' }, remove: ['minimum', 'maximum'], code: 'ZOPIA_WARN_INT64' },
177
+ uint32: { set: { format: 'uint32' }, remove: ['minimum', 'maximum'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
178
+ uint64: { set: { format: 'uint64' }, remove: ['minimum', 'maximum'], code: 'ZOPIA_WARN_CUSTOM_FORMAT' },
179
+ };
180
+ const alias = aliases[node.format];
181
+ if (alias) {
182
+ warn(alias.code ?? 'ZOPIA_WARN_CUSTOM_FORMAT', at, 'format', `format \`${node.format}\` requires an overlay for exact reverse conversion`);
183
+ const sourceKeywords = Object.fromEntries((alias.remove ?? []).filter((key) => Object.prototype.hasOwnProperty.call(node, key)).map((key) => [key, cloneJson(node[key])]));
184
+ const remove = (alias.remove ?? []).filter((key) => !Object.prototype.hasOwnProperty.call(node, key));
185
+ addOverlay({ at, set: { ...alias.set, ...sourceKeywords }, ...(remove.length ? { remove } : {}) });
186
+ } else if (!exact.has(node.format) || !acceptsString) {
187
+ warn('ZOPIA_WARN_CUSTOM_FORMAT', at, 'format', `format \`${node.format}\` has no exact Zod representation for this schema type`);
188
+ addOverlay({ at, set: { format: node.format } });
189
+ }
190
+ }
191
+
192
+ if (node.contentEncoding !== undefined) {
193
+ const acceptsString = node.type === undefined || node.type === 'string' || (Array.isArray(node.type) && node.type.includes('string'));
194
+ const supported = acceptsString && ['base64', 'base64url'].includes(String(node.contentEncoding));
195
+ if (supported) addOverlay({ at, set: { contentEncoding: node.contentEncoding }, remove: ['format', 'pattern'] });
196
+ else if (node.contentEncoding === 'hex' && acceptsString) {
197
+ warn('ZOPIA_WARN_CONTENT_ENCODING', at, 'contentEncoding', '`hex` content encoding is enforced by a pattern and restored by an overlay');
198
+ addOverlay({ at, set: { contentEncoding: 'hex' }, remove: ['pattern'] });
199
+ } else {
200
+ warn('ZOPIA_WARN_CONTENT_ENCODING', at, 'contentEncoding', `content encoding \`${String(node.contentEncoding)}\` has no exact Zod representation for this schema type`);
201
+ addOverlay({ at, set: { contentEncoding: cloneJson(node.contentEncoding) } });
202
+ }
203
+ }
204
+ if (Object.prototype.hasOwnProperty.call(node, 'contentMediaType')) {
205
+ warn('ZOPIA_WARN_CONTENT_ENCODING', at, 'contentMediaType', '`contentMediaType` is annotation-only in Zod and restored by an overlay');
206
+ addOverlay({ at, set: { contentMediaType: cloneJson(node.contentMediaType) } });
207
+ }
208
+ if (Object.prototype.hasOwnProperty.call(node, 'example') && !Object.prototype.hasOwnProperty.call(node, 'examples')) {
209
+ addOverlay({ at, set: { example: cloneJson(node.example) }, remove: ['examples'] });
210
+ }
211
+ const ignored = Object.fromEntries(['$schema', '$id', '$comment'].filter((key) => Object.prototype.hasOwnProperty.call(node, key)).map((key) => [key, cloneJson(node[key])]));
212
+ if (Object.keys(ignored).length) addOverlay({ at, set: ignored });
213
+
214
+ const applicableKeywords = new Set([
215
+ 'properties', 'required', 'additionalProperties', 'patternProperties', 'propertyNames', 'minProperties', 'maxProperties', 'dependencies', 'dependentRequired', 'dependentSchemas', 'unevaluatedProperties',
216
+ 'items', 'prefixItems', 'additionalItems', 'unevaluatedItems', 'contains', 'minContains', 'maxContains', 'minItems', 'maxItems', 'uniqueItems',
217
+ 'minLength', 'maxLength', 'pattern', 'format', 'contentEncoding', 'contentMediaType',
218
+ 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
219
+ ]);
220
+ if (node.type === undefined && Object.keys(node).some((key) => applicableKeywords.has(key))) {
221
+ freeze(node, at, 'ZOPIA_WARN_FROZEN_SUBTREE', 'type', 'keyword-only schema applicability is emitted as a Zod union and frozen for exact reverse conversion');
222
+ return;
223
+ }
224
+ if (Array.isArray(node.type)) {
225
+ freeze(node, at, 'ZOPIA_WARN_FROZEN_SUBTREE', 'type', 'type-array structure is emitted as a Zod union and frozen for exact reverse conversion');
226
+ return;
227
+ }
228
+
229
+ const visitChild = (key: string, value: unknown): void => { if (isSchemaValue(value)) visit(value, pointer(at, key), key); };
230
+ if (node.properties && typeof node.properties === 'object' && !Array.isArray(node.properties)) for (const [key, child] of Object.entries(node.properties)) if (isSchemaValue(child)) visit(child, pointer(pointer(at, 'properties'), key), 'properties');
231
+ if (node.patternProperties && typeof node.patternProperties === 'object' && !Array.isArray(node.patternProperties)) for (const [key, child] of Object.entries(node.patternProperties)) if (isSchemaValue(child)) visit(child, pointer(pointer(at, 'patternProperties'), key), 'patternProperties');
232
+ for (const key of ['additionalProperties', 'unevaluatedProperties', 'propertyNames', 'contains', 'not', 'if', 'then', 'else'] as const) visitChild(key, node[key]);
233
+ for (const key of ['items', 'prefixItems', 'allOf', 'anyOf', 'oneOf'] as const) {
234
+ const value = node[key];
235
+ if (Array.isArray(value)) value.forEach((child, index) => { if (isSchemaValue(child)) visit(child, pointer(pointer(at, key), index), key); });
236
+ else visitChild(key, value);
237
+ }
238
+ for (const key of ['$defs', 'definitions', 'dependentSchemas'] as const) if (node[key] && typeof node[key] === 'object' && !Array.isArray(node[key])) for (const [name, child] of Object.entries(node[key])) if (isSchemaValue(child)) visit(child, pointer(pointer(at, key), name), key);
239
+ };
240
+ visit(source);
241
+ return { warnings, overlays };
242
+ }
243
+
244
+ function warningCode(message: string): ZopiaWarningCode {
245
+ if (message.startsWith('oneOf')) return 'ZOPIA_WARN_ONE_OF';
246
+ if (message.startsWith('Unsupported format:')) return 'ZOPIA_WARN_CUSTOM_FORMAT';
247
+ if (message.includes('$ref')) return 'ZOPIA_WARN_REF';
248
+ if (message.startsWith('Unsupported contentEncoding:')) return 'ZOPIA_WARN_CONTENT_ENCODING';
249
+ return 'ZOPIA_WARN_INVALID_SCHEMA';
250
+ }
251
+
252
+ function warningKeyword(message: string): string {
253
+ const match = message.match(/(?:Invalid|Unsupported) ([A-Za-z$][A-Za-z0-9$]*)/);
254
+ return match?.[1] ?? (message.includes('$ref') ? '$ref' : 'schema');
255
+ }
256
+
257
+ /**
258
+ * Convert JSON Schema into executable Zod 4 code and a runtime schema.
259
+ *
260
+ * @param input JSON Schema value, JSON text, or readable `.json` path.
261
+ * @param options Root identifier and conversion settings.
262
+ * @returns Generated code, runtime schema, warnings, and round-trip overlays.
263
+ * @throws {@link ZopiaError} when the schema, file, or options are invalid.
264
+ */
265
+ export function jsonSchemaToZod(input: JsonSchema | string, options: JsonSchemaToZodOptions = {}): JsonSchemaToZodResult {
266
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new ZopiaError('ZOPIA_CONFIG_INVALID', 'JSON Schema conversion options must be an object', { at: 'options', hint: 'pass an options object or omit it' });
267
+ const unknown = Object.keys(options).find((key) => key !== 'rootName');
268
+ if (unknown) throw new ZopiaError('ZOPIA_CONFIG_INVALID', `unknown JSON Schema conversion option: ${unknown}`, { at: unknown, hint: 'remove the unsupported option' });
269
+ if (options.rootName !== undefined && typeof options.rootName !== 'string') throw new ZopiaError('ZOPIA_CONFIG_INVALID', 'rootName must be a string', { at: 'rootName', hint: 'provide a valid TypeScript identifier' });
270
+ try { return jsonSchemaToZodInternal(input, options); }
271
+ catch (error) { throw asZopiaError(error, 'ZOPIA_SCHEMA_INVALID', 'unable to convert JSON Schema', { at: '#', hint: 'fix the JSON Schema input' }); }
272
+ }
273
+
274
+ function jsonSchemaToZodInternal(input: JsonSchema | string, options: JsonSchemaToZodOptions): JsonSchemaToZodResult {
275
+ const rootName = options.rootName ?? 'schema';
276
+ const reservedNames = new Set(['arguments', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']);
277
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(rootName) || reservedNames.has(rootName)) throw new ZopiaError('ZOPIA_CONFIG_INVALID', `Invalid rootName: ${rootName}`, { at: 'rootName', hint: 'use a non-reserved TypeScript identifier' });
278
+ const warningMessages: Array<{ message: string; at: string }> = [];
279
+ let activeWarningAt = '#';
280
+ const pushWarning = (message: string): void => { warningMessages.push({ message, at: activeWarningAt }); };
281
+ const isSchema = (value: unknown): value is JsonSchema => typeof value === 'boolean' || (value !== null && typeof value === 'object' && !Array.isArray(value));
282
+ const isFiniteNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
283
+ const isNonNegativeInteger = (value: unknown): value is number => isFiniteNumber(value) && Number.isInteger(value) && value >= 0;
284
+ const jsonLiteral = (value: unknown): string | undefined => {
285
+ const active = new Set<object>();
286
+ const visit = (item: unknown): boolean => {
287
+ if (item === null || typeof item === 'string' || typeof item === 'boolean') return true;
288
+ if (typeof item === 'number') return Number.isFinite(item);
289
+ if (!item || typeof item !== 'object') return false;
290
+ if (active.has(item)) return false;
291
+ if (!Array.isArray(item) && ![Object.prototype, null].includes(Object.getPrototypeOf(item))) return false;
292
+ if (Object.getOwnPropertySymbols(item).length) return false;
293
+ active.add(item);
294
+ const valid = Array.isArray(item)
295
+ ? Object.keys(item).length === item.length && item.every((child, index) => Object.prototype.hasOwnProperty.call(item, index) && visit(child))
296
+ : Object.values(item).every(visit);
297
+ active.delete(item);
298
+ return valid;
299
+ };
300
+ if (!visit(value)) return undefined;
301
+ try {
302
+ return JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
303
+ ? Object.fromEntries(Object.entries(item).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0))
304
+ : item);
305
+ } catch { return undefined; }
306
+ };
307
+ const canonicalJson = (value: unknown): string | undefined => {
308
+ const literal = jsonLiteral(value);
309
+ if (literal === undefined) return undefined;
310
+ const normalized = JSON.parse(literal);
311
+ return JSON.stringify(normalized, (_key, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item);
312
+ };
313
+ const generatedCanonicalJson = `(value) => { if (value && typeof value === 'object' && typeof value.toJSON === 'function') return undefined; try { return JSON.stringify(value, (_key, item) => { if (item === null || typeof item === 'string' || typeof item === 'boolean') return item; if (typeof item === 'number') { if (!Number.isFinite(item)) throw new Error('non-JSON number'); return item; } if (!item || typeof item !== 'object' || Object.getOwnPropertySymbols(item).length || (!Array.isArray(item) && ![Object.prototype, null].includes(Object.getPrototypeOf(item)))) throw new Error('non-JSON value'); const children = Array.isArray(item) ? item : Object.values(item); if (children.some((child) => child && typeof child === 'object' && typeof child.toJSON === 'function')) throw new Error('non-JSON toJSON'); if (Array.isArray(item)) { if (Object.keys(item).length !== item.length || !item.every((_child, index) => Object.prototype.hasOwnProperty.call(item, index))) throw new Error('non-JSON array'); return item; } return Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)); }); } catch { return undefined; } }`;
314
+ let source: JsonSchema;
315
+ try {
316
+ if (typeof input !== 'string') source = input;
317
+ else {
318
+ const text = input.toLowerCase().endsWith('.json') ? readFileSync(input, 'utf8') : input;
319
+ source = JSON.parse(text) as JsonSchema;
320
+ }
321
+ } catch (error) { throw new ZopiaError('ZOPIA_SCHEMA_INVALID', `Invalid JSON Schema input: ${error instanceof Error ? error.message : String(error)}`, { at: typeof input === 'string' && input.toLowerCase().endsWith('.json') ? input : '#', cause: error }); }
322
+ if (!isSchema(source)) throw new ZopiaError('ZOPIA_SCHEMA_INVALID', 'Invalid JSON Schema input: expected an object or boolean schema');
323
+ const analysis = analyzeSchema(source);
324
+ if (typeof source === 'object') for (const keyword of ['$defs', 'definitions'] as const) {
325
+ if (!Object.prototype.hasOwnProperty.call(source, keyword)) continue;
326
+ const definitions = source[keyword];
327
+ const at = pointer('#', keyword);
328
+ if (!definitions || typeof definitions !== 'object' || Array.isArray(definitions)) {
329
+ warningMessages.push({ at, message: `Invalid ${keyword}: expected an object of schemas` });
330
+ continue;
331
+ }
332
+ for (const [name, definition] of Object.entries(definitions)) if (!isSchema(definition)) warningMessages.push({
333
+ at: pointer(at, name),
334
+ message: `Invalid ${keyword} entry ${name}: expected a schema`,
335
+ });
336
+ }
337
+ const resolveLocalRef = (ref: string): JsonSchema | undefined => {
338
+ if (ref !== '#' && !ref.startsWith('#/')) return undefined;
339
+ if (ref === '#') return source;
340
+ return ref.slice(2).split('/').map((part) => {
341
+ try { return decodeJsonPointerSegment(part, ref); }
342
+ catch { return undefined; }
343
+ }).reduce<any>((value, key) => {
344
+ if (key === undefined || value === null || (typeof value !== 'object' && typeof value !== 'function') || !Object.prototype.hasOwnProperty.call(value, key)) return undefined;
345
+ return value[key];
346
+ }, source);
347
+ };
348
+ type Converted = { schema: z.ZodType; code: string };
349
+ let convert: (node: JsonSchema, resolving?: Set<string>, at?: string) => Converted;
350
+ const definitionEntries: Array<{ ref: string; name: string; schema: JsonSchema }> = [];
351
+ const usedNames = new Set([rootName]);
352
+ if (typeof source === 'object') for (const keyword of ['$defs', 'definitions'] as const) {
353
+ const definitions = source[keyword];
354
+ if (!definitions || typeof definitions !== 'object' || Array.isArray(definitions)) continue;
355
+ for (const [rawName, definition] of sortedEntries(definitions)) {
356
+ if (!isSchema(definition)) continue;
357
+ const words = rawName.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
358
+ let name = words.map((word, index) => index === 0 ? word[0]?.toLowerCase() + word.slice(1) : word[0]?.toUpperCase() + word.slice(1)).join('') || 'definition';
359
+ if (!/^[A-Za-z_$]/.test(name) || reservedNames.has(name)) name = `definition${name[0]?.toUpperCase() ?? ''}${name.slice(1)}`;
360
+ const base = name; let suffix = 2;
361
+ while (usedNames.has(name)) name = `${base}${suffix++}`;
362
+ usedNames.add(name);
363
+ definitionEntries.push({ ref: `#/${keyword}/${rawName.replace(/~/g, '~0').replace(/\//g, '~1')}`, name, schema: definition });
364
+ }
365
+ }
366
+ const definitionNames = new Map(definitionEntries.map((entry) => [entry.ref, entry.name]));
367
+ const usedDefinitionRefs = new Set<string>();
368
+ const definitionResults = new Map<string, Converted>();
369
+ let rootResult: Converted | undefined;
370
+
371
+ const convertCore = (node: JsonSchema, resolving = new Set<string>(), at = '#'): Converted => {
372
+ if (node === true) return { schema: z.any(), code: 'z.any()' };
373
+ if (node === false) return { schema: z.never(), code: 'z.never()' };
374
+ if (!node || typeof node !== 'object' || Array.isArray(node)) { pushWarning('Schema node is not an object'); return { schema: z.any(), code: 'z.any()' }; }
375
+ if (Object.keys(node).length === 1 && node.not !== null && typeof node.not === 'object' && !Array.isArray(node.not) && Object.keys(node.not).length === 0) return { schema: z.never(), code: 'z.never()' };
376
+ if (node.nullable !== undefined) {
377
+ const withoutNullable = Object.fromEntries(Object.entries(node).filter(([key]) => key !== 'nullable')) as JsonSchema;
378
+ if (typeof node.nullable !== 'boolean') { pushWarning('Invalid nullable: expected a boolean'); return convert(withoutNullable, resolving, at); }
379
+ const converted = convert(withoutNullable, resolving, at);
380
+ return node.nullable ? { schema: converted.schema.nullable(), code: `${converted.code}.nullable()` } : converted;
381
+ }
382
+ if (Object.prototype.hasOwnProperty.call(node, 'default')) {
383
+ const withoutDefault = Object.fromEntries(Object.entries(node).filter(([key]) => key !== 'default')) as JsonSchema;
384
+ const converted = convert(withoutDefault, resolving, at);
385
+ const literal = jsonLiteral(node.default);
386
+ if (literal === undefined) { pushWarning('Invalid default: expected a JSON value'); return converted; }
387
+ const value = JSON.parse(literal);
388
+ return { schema: converted.schema.default(value), code: `${converted.code}.default(${literal})` };
389
+ }
390
+ if ('$ref' in node) {
391
+ if (typeof node.$ref !== 'string' || node.$ref.length === 0) { pushWarning('Invalid $ref: expected a non-empty string'); return { schema: z.any(), code: 'z.any()' }; }
392
+ const ref = node.$ref; const target = resolveLocalRef(ref);
393
+ if (!target) { pushWarning(`Unsupported $ref: ${ref}`); return { schema: z.any(), code: 'z.any()' }; }
394
+ const siblings = Object.fromEntries(Object.entries(node).filter(([key]) => !['$ref', '$defs', 'definitions', '$schema', '$id', '$comment', 'title', 'description', 'examples', 'example', 'readOnly', 'writeOnly', 'deprecated', 'discriminator', 'xml', 'externalDocs'].includes(key) && !key.startsWith('x-')));
395
+ const definitionName = definitionNames.get(ref);
396
+ if (definitionName) {
397
+ usedDefinitionRefs.add(ref);
398
+ const referenced: Converted = { schema: z.lazy(() => definitionResults.get(ref)?.schema ?? z.never()), code: `z.lazy(() => ${definitionName})` };
399
+ if (Object.keys(siblings).length === 0) return referenced;
400
+ const sibling = convert(siblings, resolving, at);
401
+ return { schema: referenced.schema.and(sibling.schema), code: `${referenced.code}.and(${sibling.code})` };
402
+ }
403
+ if (ref === '#') {
404
+ const referenced: Converted = { schema: z.lazy(() => rootResult?.schema ?? z.never()), code: `z.lazy(() => ${rootName})` };
405
+ if (Object.keys(siblings).length === 0) return referenced;
406
+ const sibling = convert(siblings, resolving, at);
407
+ return { schema: referenced.schema.and(sibling.schema), code: `${referenced.code}.and(${sibling.code})` };
408
+ }
409
+ if (resolving.has(ref)) { pushWarning(`Recursive $ref cannot be materialized outside a named definition: ${ref}`); return { schema: z.any(), code: 'z.any()' }; }
410
+ const referenced = convert(target, new Set(resolving).add(ref), ref);
411
+ if (Object.keys(siblings).length === 0) return referenced;
412
+ const sibling = convert(siblings, resolving, at);
413
+ return { schema: referenced.schema.and(sibling.schema), code: `${referenced.code}.and(${sibling.code})` };
414
+ }
415
+ if (Array.isArray(node.type)) {
416
+ const allowedTypes = new Set(['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']);
417
+ if (node.type.length === 0 || !node.type.every((type: unknown) => typeof type === 'string' && allowedTypes.has(type))) { pushWarning('Invalid type: expected a non-empty array of valid JSON Schema type names'); return { schema: z.any(), code: 'z.any()' }; }
418
+ const variants = node.type.map((type: string) => {
419
+ const branch: JsonSchema = { ...node, type };
420
+ if (type === 'null') {
421
+ delete branch.format; delete branch.minLength; delete branch.maxLength;
422
+ delete branch.pattern; delete branch.minimum; delete branch.maximum;
423
+ delete branch.exclusiveMinimum; delete branch.exclusiveMaximum;
424
+ }
425
+ return convert(branch, resolving, at);
426
+ });
427
+ if (variants.length === 1) return variants[0];
428
+ return { schema: z.union(variants.map((item: { schema: z.ZodType }) => item.schema) as [z.ZodType, z.ZodType, ...z.ZodType[]]), code: `z.union([${variants.map((item: { code: string }) => item.code).join(', ')}])` };
429
+ }
430
+ const withSiblings = (keyword: string, constrained: { schema: z.ZodType; code: string }, ignored: string[] = []): { schema: z.ZodType; code: string } => {
431
+ const siblings = Object.fromEntries(Object.entries(node).filter(([key]) => key !== keyword && !ignored.includes(key)));
432
+ if (Object.keys(siblings).length === 0) return constrained;
433
+ const sibling = convert(siblings, resolving, at);
434
+ return { schema: (sibling.schema as any).and(constrained.schema), code: `${sibling.code}.and(${constrained.code})` };
435
+ };
436
+ const literalSchema = (value: unknown, keyword: 'enum' | 'const'): { schema: z.ZodType; code: string } | undefined => {
437
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || isFiniteNumber(value)) return { schema: z.literal(value as any), code: `z.literal(${JSON.stringify(value)})` };
438
+ if (value && typeof value === 'object') {
439
+ const literal = jsonLiteral(value);
440
+ if (literal !== undefined) {
441
+ const normalized = JSON.parse(literal);
442
+ const canonical = canonicalJson(normalized)!;
443
+ const metadata = keyword === 'enum' ? [normalized] : normalized;
444
+ return {
445
+ schema: z.any().refine((candidate) => canonicalJson(candidate) === canonical).meta({ [keyword]: metadata }),
446
+ code: `z.any().refine((value) => (${generatedCanonicalJson})(value) === ${JSON.stringify(canonical)}).meta({ ${keyword}: ${JSON.stringify(metadata)} })`,
447
+ };
448
+ }
449
+ }
450
+ pushWarning(`Invalid ${keyword} value: expected a JSON value`);
451
+ return undefined;
452
+ };
453
+ if ('oneOf' in node || 'anyOf' in node) {
454
+ const key = 'oneOf' in node ? 'oneOf' : 'anyOf';
455
+ if (!Array.isArray(node[key])) { pushWarning(`Invalid ${key}: expected an array`); return withSiblings(key, { schema: z.any(), code: 'z.any()' }); }
456
+ const items = node[key].map((child: JsonSchema, index: number) => convert(child, resolving, pointer(pointer(at, key), index)));
457
+ const propertyName = key === 'oneOf' && node.discriminator && typeof node.discriminator === 'object' && typeof node.discriminator.propertyName === 'string' ? node.discriminator.propertyName : undefined;
458
+ const discriminated = propertyName !== undefined && (node[key] as JsonSchema[]).every((child) => {
459
+ const resolved = typeof child === 'object' && typeof child.$ref === 'string' ? resolveLocalRef(child.$ref) : child;
460
+ if (!resolved || typeof resolved === 'boolean' || resolved.type !== 'object' || !resolved.properties || typeof resolved.properties !== 'object') return false;
461
+ const discriminator = resolved.properties[propertyName];
462
+ return discriminator && typeof discriminator === 'object' && (Object.prototype.hasOwnProperty.call(discriminator, 'const') || Array.isArray(discriminator.enum));
463
+ });
464
+ let combined: Converted;
465
+ if (discriminated && items.length > 1) {
466
+ try {
467
+ combined = {
468
+ schema: z.discriminatedUnion(propertyName!, items.map((item) => item.schema) as any),
469
+ code: `z.discriminatedUnion(${JSON.stringify(propertyName)}, [${items.map((item) => item.code).join(', ')}])`,
470
+ };
471
+ } catch {
472
+ pushWarning('oneOf discriminator could not be represented by z.discriminatedUnion; z.union was used');
473
+ combined = { schema: z.union(items.map((item) => item.schema) as [z.ZodType, z.ZodType, ...z.ZodType[]]), code: `z.union([${items.map((item) => item.code).join(', ')}])` };
474
+ }
475
+ } else {
476
+ if (key === 'oneOf') pushWarning('oneOf is approximated by z.union and does not enforce exclusivity');
477
+ combined = items.length === 0
478
+ ? { schema: z.never(), code: 'z.never()' }
479
+ : items.length === 1
480
+ ? items[0]
481
+ : { schema: z.union(items.map((item: { schema: z.ZodType }) => item.schema) as [z.ZodType, z.ZodType, ...z.ZodType[]]), code: `z.union([${items.map((item: { code: string }) => item.code).join(', ')}])` };
482
+ }
483
+ return withSiblings(key, combined, propertyName ? ['discriminator'] : []);
484
+ }
485
+ if ('allOf' in node) {
486
+ if (!Array.isArray(node.allOf)) { pushWarning('Invalid allOf: expected an array'); return withSiblings('allOf', { schema: z.any(), code: 'z.any()' }); }
487
+ const items = node.allOf.map((child: JsonSchema, index: number) => convert(child, resolving, pointer(pointer(at, 'allOf'), index)));
488
+ const combined = items.length === 0
489
+ ? { schema: z.any(), code: 'z.any()' }
490
+ : { schema: items.slice(1).reduce((acc: any, item: { schema: z.ZodType }) => acc.and(item.schema), items[0].schema as any), code: items.slice(1).reduce((code: string, item: { code: string }) => `${code}.and(${item.code})`, items[0].code) };
491
+ return withSiblings('allOf', combined);
492
+ }
493
+ if ('enum' in node) {
494
+ if (!Array.isArray(node.enum)) { pushWarning('Invalid enum: expected an array'); return withSiblings('enum', { schema: z.any(), code: 'z.any()' }); }
495
+ let combined: { schema: z.ZodType; code: string };
496
+ if (node.enum.length === 0) combined = { schema: z.never(), code: 'z.never()' };
497
+ else if (node.enum.every((value: unknown) => typeof value === 'string')) combined = { schema: z.enum(node.enum as [string, ...string[]]), code: `z.enum(${JSON.stringify(node.enum)})` };
498
+ else if (node.enum.some((value: unknown) => value !== null && typeof value === 'object')) {
499
+ const values = node.enum.flatMap((value: unknown) => {
500
+ const literal = jsonLiteral(value);
501
+ if (literal === undefined) { pushWarning('Invalid enum value: expected a JSON value'); return []; }
502
+ return [JSON.parse(literal)];
503
+ });
504
+ if (values.length === 0) combined = { schema: z.never(), code: 'z.never()' };
505
+ else {
506
+ const canonicals = values.map((value: unknown) => canonicalJson(value)!);
507
+ combined = {
508
+ schema: z.any().refine((candidate) => { const canonical = canonicalJson(candidate); return canonical !== undefined && canonicals.includes(canonical); }).meta({ enum: values }),
509
+ code: `z.any().refine((value) => { const canonical = (${generatedCanonicalJson})(value); return canonical !== undefined && ${JSON.stringify(canonicals)}.includes(canonical); }).meta({ enum: ${JSON.stringify(values)} })`,
510
+ };
511
+ }
512
+ } else {
513
+ const literals = node.enum.map((value: unknown) => literalSchema(value, 'enum')).filter((item: { schema: z.ZodType; code: string } | undefined): item is { schema: z.ZodType; code: string } => item !== undefined);
514
+ combined = literals.length === 0
515
+ ? { schema: z.never(), code: 'z.never()' }
516
+ : literals.length === 1
517
+ ? literals[0]
518
+ : { schema: z.union(literals.map((item) => item.schema) as [z.ZodType, z.ZodType, ...z.ZodType[]]), code: `z.union([${literals.map((item) => item.code).join(', ')}])` };
519
+ }
520
+ const stringConstraints = ['minLength', 'maxLength', 'pattern', 'format', 'contentEncoding', 'contentMediaType'];
521
+ const redundantType = node.type === 'string' && node.enum.length > 0 && node.enum.every((value: unknown) => typeof value === 'string') && !stringConstraints.some((key) => Object.prototype.hasOwnProperty.call(node, key));
522
+ return withSiblings('enum', combined, redundantType ? ['type'] : []);
523
+ }
524
+ if ('const' in node) {
525
+ const literal = literalSchema(node.const, 'const') ?? { schema: z.any(), code: 'z.any()' };
526
+ return withSiblings('const', literal);
527
+ }
528
+ const keywordGroups = [
529
+ { type: 'object', keywords: new Set(['properties', 'required', 'additionalProperties', 'patternProperties', 'propertyNames', 'minProperties', 'maxProperties', 'dependencies', 'dependentRequired', 'dependentSchemas', 'unevaluatedProperties']) },
530
+ { type: 'array', keywords: new Set(['items', 'prefixItems', 'additionalItems', 'unevaluatedItems', 'contains', 'minContains', 'maxContains', 'minItems', 'maxItems', 'uniqueItems']) },
531
+ { type: 'string', keywords: new Set(['minLength', 'maxLength', 'pattern', 'format', 'contentEncoding', 'contentMediaType']) },
532
+ { type: 'number', keywords: new Set(['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf']) },
533
+ ] as const;
534
+ const numberFormats = new Set(['int32', 'int64', 'uint32', 'uint64']);
535
+ const keywordGroup = node.type === undefined
536
+ ? numberFormats.has(String(node.format)) ? keywordGroups[3] : keywordGroups.find((group) => Object.keys(node).some((key) => group.keywords.has(key)))
537
+ : undefined;
538
+ if (keywordGroup) {
539
+ const typedNode = { type: keywordGroup.type, ...Object.fromEntries(Object.entries(node).filter(([key]) => keywordGroup.keywords.has(key))) } as JsonSchema;
540
+ const typedResult = convert(typedNode, resolving, at);
541
+ const jsonTypes = [
542
+ { type: 'object', schema: z.object({}).passthrough(), code: 'z.object({}).passthrough()' },
543
+ { type: 'array', schema: z.array(z.any()), code: 'z.array(z.any())' },
544
+ { type: 'string', schema: z.string(), code: 'z.string()' },
545
+ { type: 'number', schema: z.number(), code: 'z.number()' },
546
+ { type: 'boolean', schema: z.boolean(), code: 'z.boolean()' },
547
+ { type: 'null', schema: z.null(), code: 'z.null()' },
548
+ ];
549
+ const alternatives = [{ type: keywordGroup.type, schema: typedResult.schema, code: typedResult.code }, ...jsonTypes.filter((candidate) => candidate.type !== keywordGroup.type)];
550
+ const applicable = {
551
+ schema: z.union(alternatives.map((candidate) => candidate.schema) as [z.ZodType, z.ZodType, ...z.ZodType[]]),
552
+ code: `z.union([${alternatives.map((candidate) => candidate.code).join(', ')}])`,
553
+ };
554
+ const siblings = Object.fromEntries(Object.entries(node).filter(([key]) => !keywordGroup.keywords.has(key)));
555
+ if (Object.keys(siblings).length === 0) return applicable;
556
+ const siblingResult = convert(siblings, resolving, at);
557
+ return { schema: (siblingResult.schema as any).and(applicable.schema), code: `${siblingResult.code}.and(${applicable.code})` };
558
+ }
559
+ let result: { schema: z.ZodType; code: string };
560
+ let arrayIsTuple = false;
561
+ switch (node.type) {
562
+ case 'object': {
563
+ if (isNativeRecordObject(node)) {
564
+ const key = convert(node.propertyNames as JsonSchema, resolving, pointer(at, 'propertyNames'));
565
+ const value = convert(node.additionalProperties as JsonSchema, resolving, pointer(at, 'additionalProperties'));
566
+ result = { schema: z.record(key.schema as any, value.schema), code: `z.record(${key.code}, ${value.code})` };
567
+ break;
568
+ }
569
+ if (node.properties !== undefined && (!node.properties || typeof node.properties !== 'object' || Array.isArray(node.properties))) { pushWarning('Invalid properties: expected an object'); result = { schema: z.object({}).passthrough(), code: 'z.object({}).passthrough()' }; break; }
570
+ const requiredKeys = Array.isArray(node.required) && node.required.every((key: unknown) => typeof key === 'string') ? node.required as string[] : [];
571
+ if (node.required !== undefined && (!Array.isArray(node.required) || requiredKeys.length !== node.required.length)) pushWarning('Invalid required: expected an array of strings');
572
+ const patternPropertiesValid = node.patternProperties === undefined || (node.patternProperties !== null && typeof node.patternProperties === 'object' && !Array.isArray(node.patternProperties));
573
+ if (!patternPropertiesValid) pushWarning('Invalid patternProperties: expected an object');
574
+ const additionalPropertiesValid = node.additionalProperties === undefined || isSchema(node.additionalProperties);
575
+ if (!additionalPropertiesValid) pushWarning('Invalid additionalProperties: expected a schema');
576
+ const unevaluatedPropertiesValid = node.unevaluatedProperties === undefined || isSchema(node.unevaluatedProperties);
577
+ if (!unevaluatedPropertiesValid) pushWarning('Invalid unevaluatedProperties: expected a schema');
578
+ const shape: Record<string, z.ZodType> = Object.create(null); const codeByProperty = new Map<string, string>();
579
+ const declaredKeys = new Set(Object.keys(node.properties ?? {}));
580
+ const undeclaredRequiredKeys = requiredKeys.filter((key) => !declaredKeys.has(key));
581
+ for (const [key, child] of Object.entries(node.properties ?? {})) {
582
+ const item = convert(child as JsonSchema, resolving, pointer(pointer(at, 'properties'), key));
583
+ const required = requiredKeys.includes(key);
584
+ shape[key] = required ? item.schema : item.schema.optional();
585
+ codeByProperty.set(key, `[${JSON.stringify(key)}]: ${required ? item.code : `${item.code}.optional()`}`);
586
+ }
587
+ for (const key of undeclaredRequiredKeys) { shape[key] = z.unknown(); codeByProperty.set(key, `[${JSON.stringify(key)}]: z.unknown()`); }
588
+ const parts = [...codeByProperty].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, code]) => code);
589
+ let objectSchema = z.object(shape); let objectCode = `z.object({ ${parts.join(', ')} })`;
590
+ const patternSchemas: Array<{ expression: string; regex: RegExp; converted: Converted }> = [];
591
+ if (patternPropertiesValid && node.patternProperties) {
592
+ for (const [expression, patternNode] of sortedEntries(node.patternProperties as Record<string, JsonSchema>)) {
593
+ try {
594
+ const regex = new RegExp(expression);
595
+ patternSchemas.push({ expression, regex, converted: convert(patternNode, resolving, pointer(pointer(at, 'patternProperties'), expression)) });
596
+ } catch { pushWarning(`Invalid patternProperties expression: ${expression}`); }
597
+ }
598
+ }
599
+ if (patternSchemas.length) {
600
+ objectSchema = objectSchema.passthrough(); objectCode += '.passthrough()';
601
+ for (const pattern of patternSchemas) {
602
+ objectSchema = objectSchema.refine((value) => Object.entries(value).every(([key, item]) => !pattern.regex.test(key) || pattern.converted.schema.safeParse(item).success));
603
+ objectCode += `.refine((value) => Object.entries(value).every(([key, item]) => !(new RegExp(${JSON.stringify(pattern.expression)})).test(key) || (${pattern.converted.code}).safeParse(item).success))`;
604
+ }
605
+ const knownKeys = declaredKeys;
606
+ const isEvaluated = (key: string): boolean => knownKeys.has(key) || patternSchemas.some((pattern) => pattern.regex.test(key));
607
+ const knownCode = JSON.stringify([...knownKeys]);
608
+ const patternCode = `[${patternSchemas.map((pattern) => `new RegExp(${JSON.stringify(pattern.expression)})`).join(', ')}]`;
609
+ if (node.additionalProperties === false || (node.additionalProperties === undefined && node.unevaluatedProperties === false)) {
610
+ objectSchema = objectSchema.refine((value) => Object.keys(value).every(isEvaluated));
611
+ objectCode += `.refine((value) => Object.keys(value).every((key) => ${knownCode}.includes(key) || ${patternCode}.some((pattern) => pattern.test(key))))`;
612
+ } else if (additionalPropertiesValid && node.additionalProperties !== undefined && typeof node.additionalProperties !== 'boolean') {
613
+ const item = convert(node.additionalProperties, resolving, pointer(at, 'additionalProperties'));
614
+ objectSchema = objectSchema.refine((value) => Object.entries(value).every(([key, candidate]) => isEvaluated(key) || item.schema.safeParse(candidate).success));
615
+ objectCode += `.refine((value) => Object.entries(value).every(([key, item]) => ${knownCode}.includes(key) || ${patternCode}.some((pattern) => pattern.test(key)) || (${item.code}).safeParse(item).success))`;
616
+ } else if (node.additionalProperties === undefined && unevaluatedPropertiesValid && node.unevaluatedProperties !== undefined && typeof node.unevaluatedProperties !== 'boolean') {
617
+ const item = convert(node.unevaluatedProperties, resolving, pointer(at, 'unevaluatedProperties'));
618
+ objectSchema = objectSchema.refine((value) => Object.entries(value).every(([key, candidate]) => isEvaluated(key) || item.schema.safeParse(candidate).success));
619
+ objectCode += `.refine((value) => Object.entries(value).every(([key, item]) => ${knownCode}.includes(key) || ${patternCode}.some((pattern) => pattern.test(key)) || (${item.code}).safeParse(item).success))`;
620
+ }
621
+ } else if (node.additionalProperties === false || (node.additionalProperties === undefined && node.unevaluatedProperties === false)) { objectSchema = objectSchema.strict(); objectCode += '.strict()'; }
622
+ else if (additionalPropertiesValid && node.additionalProperties !== undefined && typeof node.additionalProperties !== 'boolean') { const item = convert(node.additionalProperties, resolving, pointer(at, 'additionalProperties')); objectSchema = objectSchema.catchall(item.schema); objectCode += `.catchall(${item.code})`; }
623
+ else if (node.additionalProperties === undefined && unevaluatedPropertiesValid && node.unevaluatedProperties !== undefined && typeof node.unevaluatedProperties !== 'boolean') { const item = convert(node.unevaluatedProperties, resolving, pointer(at, 'unevaluatedProperties')); objectSchema = objectSchema.catchall(item.schema); objectCode += `.catchall(${item.code})`; }
624
+ else { objectSchema = objectSchema.passthrough(); objectCode += '.passthrough()'; }
625
+ if (patternSchemas.length === 0 && undeclaredRequiredKeys.length) {
626
+ if (node.additionalProperties === false || (node.additionalProperties === undefined && node.unevaluatedProperties === false)) {
627
+ objectSchema = objectSchema.refine((value) => undeclaredRequiredKeys.every((key) => !Object.prototype.hasOwnProperty.call(value, key)));
628
+ objectCode += `.refine((value) => ${JSON.stringify(undeclaredRequiredKeys)}.every((key) => !Object.prototype.hasOwnProperty.call(value, key)))`;
629
+ } else if (additionalPropertiesValid && node.additionalProperties !== undefined && typeof node.additionalProperties !== 'boolean') {
630
+ const item = convert(node.additionalProperties, resolving, pointer(at, 'additionalProperties'));
631
+ objectSchema = objectSchema.refine((value) => undeclaredRequiredKeys.every((key) => item.schema.safeParse(value[key]).success));
632
+ objectCode += `.refine((value) => ${JSON.stringify(undeclaredRequiredKeys)}.every((key) => (${item.code}).safeParse(value[key]).success))`;
633
+ } else if (node.additionalProperties === undefined && unevaluatedPropertiesValid && node.unevaluatedProperties !== undefined && typeof node.unevaluatedProperties !== 'boolean') {
634
+ const item = convert(node.unevaluatedProperties, resolving, pointer(at, 'unevaluatedProperties'));
635
+ objectSchema = objectSchema.refine((value) => undeclaredRequiredKeys.every((key) => item.schema.safeParse(value[key]).success));
636
+ objectCode += `.refine((value) => ${JSON.stringify(undeclaredRequiredKeys)}.every((key) => (${item.code}).safeParse(value[key]).success))`;
637
+ }
638
+ }
639
+ result = { schema: objectSchema, code: objectCode }; break;
640
+ }
641
+ case 'array': {
642
+ const prefixItemsValid = node.prefixItems === undefined || Array.isArray(node.prefixItems);
643
+ if (!prefixItemsValid) pushWarning('Invalid prefixItems: expected an array of schemas');
644
+ const itemsValid = node.items === undefined || Array.isArray(node.items) || isSchema(node.items);
645
+ if (!itemsValid) pushWarning('Invalid items: expected a schema or tuple array');
646
+ const additionalItemsValid = node.additionalItems === undefined || isSchema(node.additionalItems);
647
+ if (!additionalItemsValid) pushWarning('Invalid additionalItems: expected a schema');
648
+ const unevaluatedItemsValid = node.unevaluatedItems === undefined || isSchema(node.unevaluatedItems);
649
+ if (!unevaluatedItemsValid) pushWarning('Invalid unevaluatedItems: expected a schema');
650
+ if (itemsValid && node.items === false && !Array.isArray(node.prefixItems)) { arrayIsTuple = true; result = { schema: z.tuple([]), code: 'z.tuple([])' }; break; }
651
+ if (Array.isArray(node.prefixItems) || (itemsValid && Array.isArray(node.items))) {
652
+ arrayIsTuple = true;
653
+ const usesPrefixItems = Array.isArray(node.prefixItems);
654
+ const tupleNodes = (usesPrefixItems ? node.prefixItems : node.items) as JsonSchema[];
655
+ const tupleKeyword = usesPrefixItems ? 'prefixItems' : 'items';
656
+ const tuple = tupleNodes.map((item, index) => convert(item, resolving, pointer(pointer(at, tupleKeyword), index)));
657
+ const requiredItems = isNonNegativeInteger(node.minItems) ? Math.min(node.minItems, tuple.length) : 0;
658
+ const schemas = tuple.map((item, index) => index < requiredItems ? item.schema : item.schema.optional());
659
+ const codes = tuple.map((item, index) => index < requiredItems ? item.code : `${item.code}.optional()`);
660
+ let tupleSchema: any = z.tuple(schemas as any); let tupleCode = `z.tuple([${codes.join(', ')}])`;
661
+ const restNode: JsonSchema = usesPrefixItems
662
+ ? itemsValid && node.items !== undefined ? node.items : unevaluatedItemsValid && node.unevaluatedItems !== undefined ? node.unevaluatedItems : true
663
+ : additionalItemsValid && node.additionalItems !== undefined ? node.additionalItems : true;
664
+ const restKeyword = usesPrefixItems ? node.items !== undefined ? 'items' : node.unevaluatedItems !== undefined ? 'unevaluatedItems' : undefined : node.additionalItems !== undefined ? 'additionalItems' : undefined;
665
+ if (restNode !== false) { const rest = convert(restNode, resolving, restKeyword ? pointer(at, restKeyword) : at); tupleSchema = tupleSchema.rest(rest.schema); tupleCode += `.rest(${rest.code})`; }
666
+ result = { schema: tupleSchema, code: tupleCode };
667
+ } else { const item = convert(itemsValid && node.items !== undefined ? node.items as JsonSchema : true, resolving, node.items !== undefined ? pointer(at, 'items') : at); result = { schema: z.array(item.schema), code: `z.array(${item.code})` }; }
668
+ break;
669
+ }
670
+ case 'string': result = { schema: z.string(), code: 'z.string()' }; break;
671
+ case 'number': result = { schema: z.number(), code: 'z.number()' }; break;
672
+ case 'integer': result = { schema: z.number().int(), code: 'z.number().int()' }; break;
673
+ case 'boolean': result = { schema: z.boolean(), code: 'z.boolean()' }; break;
674
+ case 'null': result = { schema: z.null(), code: 'z.null()' }; break;
675
+ default:
676
+ if (node.type !== undefined) pushWarning(`Unsupported JSON Schema type: ${String(node.type)}`);
677
+ result = { schema: z.any(), code: 'z.any()' };
678
+ }
679
+ if (Object.prototype.hasOwnProperty.call(node, 'not')) {
680
+ if (!isSchema(node.not)) pushWarning('Invalid not: expected a schema');
681
+ else if (node.not !== false) {
682
+ const excluded = convert(node.not, resolving, pointer(at, 'not'));
683
+ result = { schema: result.schema.refine((value: unknown) => !excluded.schema.safeParse(value).success), code: `${result.code}.refine((value) => !(${excluded.code}).safeParse(value).success)` };
684
+ }
685
+ }
686
+ if ((node.format === 'int32' || node.format === 'int64') && (node.type === 'integer' || node.type === 'number')) {
687
+ const integer = (result.schema as any).int(); const code = `${result.code}.int()`;
688
+ result = node.format === 'int32' ? { schema: integer.min(-2147483648).max(2147483647), code: `${code}.min(-2147483648).max(2147483647)` } : { schema: integer, code };
689
+ }
690
+ if ((node.format === 'uint32' || node.format === 'uint64') && (node.type === 'integer' || node.type === 'number')) {
691
+ const integer = (result.schema as any).int().nonnegative(); const code = `${result.code}.int().nonnegative()`;
692
+ result = node.format === 'uint32' ? { schema: integer.max(4294967295), code: `${code}.max(4294967295)` } : { schema: integer, code };
693
+ }
694
+ if (node.format && node.type === 'string') {
695
+ const formats: Record<string, { schema: (s: any) => any; code: string; replace?: boolean }> = {
696
+ email: { schema: (s) => s.email(), code: 'email()' },
697
+ uuid: { schema: (s) => s.uuid(), code: 'uuid()' },
698
+ uri: { schema: (s) => s.url(), code: 'url()' },
699
+ url: { schema: (s) => s.url(), code: 'url()' },
700
+ 'date-time': { schema: (s) => s.datetime(), code: 'datetime()' },
701
+ date: { schema: (s) => s.date(), code: 'date()' },
702
+ hostname: { schema: () => z.hostname(), code: 'z.hostname()', replace: true },
703
+ ipv4: { schema: () => z.ipv4(), code: 'z.ipv4()', replace: true },
704
+ ipv6: { schema: () => z.ipv6(), code: 'z.ipv6()', replace: true },
705
+ base64: { schema: (s) => s.base64(), code: 'base64()' },
706
+ byte: { schema: (s) => s.base64(), code: 'base64()' },
707
+ 'base64url': { schema: (s) => s.base64url(), code: 'base64url()' },
708
+ emoji: { schema: (s) => s.emoji(), code: 'emoji()' },
709
+ time: { schema: (s) => s.time(), code: 'time()' },
710
+ duration: { schema: (s) => s.duration(), code: 'duration()' },
711
+ };
712
+ const format = formats[node.format];
713
+ if (format) {
714
+ try {
715
+ if (format.replace && result.code === 'z.string()') result = { schema: format.schema(result.schema), code: format.code };
716
+ else if (format.replace) result = { schema: result.schema.and(format.schema(result.schema)), code: `${result.code}.and(${format.code})` };
717
+ else result = { schema: format.schema(result.schema), code: `${result.code}.${format.code}` };
718
+ } catch { pushWarning(`Unsupported format: ${node.format}`); }
719
+ } else pushWarning(`Unsupported format: ${node.format}`);
720
+ }
721
+ if (node.uniqueItems !== undefined && node.type === 'array' && typeof node.uniqueItems !== 'boolean') pushWarning('Invalid uniqueItems: expected a boolean');
722
+ if (node.uniqueItems === true && node.type === 'array') {
723
+ result = {
724
+ schema: result.schema.refine((value: unknown) => {
725
+ const items = value as unknown[];
726
+ const canonicals = items.map((item) => canonicalJson(item));
727
+ return canonicals.every((canonical) => canonical !== undefined) && new Set(canonicals).size === items.length;
728
+ }),
729
+ code: `${result.code}.superRefine((items, ctx) => { const canonicals = items.map((item) => (${generatedCanonicalJson})(item)); if (canonicals.some((canonical) => canonical === undefined) || new Set(canonicals).size !== items.length) ctx.addIssue({ code: 'custom', message: 'Array items must be unique JSON values' }); })`,
730
+ };
731
+ }
732
+ const applyConstraint = (name: string, value: unknown, valid: boolean, expected: string, apply: (schema: any, constraint: any) => any, method: string): void => {
733
+ if (value === undefined) return;
734
+ if (!valid) { pushWarning(`Invalid ${name}: expected ${expected}`); return; }
735
+ try { result = { schema: apply(result.schema, value), code: `${result.code}.${method}(${JSON.stringify(value)})` }; }
736
+ catch { pushWarning(`Unsupported constraint: ${name}`); }
737
+ };
738
+ if (node.type === 'string') {
739
+ applyConstraint('minLength', node.minLength, isNonNegativeInteger(node.minLength), 'a non-negative integer', (schema, value) => schema.min(value), 'min');
740
+ applyConstraint('maxLength', node.maxLength, isNonNegativeInteger(node.maxLength), 'a non-negative integer', (schema, value) => schema.max(value), 'max');
741
+ if (node.pattern !== undefined) {
742
+ if (typeof node.pattern !== 'string') pushWarning('Invalid pattern: expected a string');
743
+ else try { result = { schema: (result.schema as any).regex(new RegExp(node.pattern)), code: `${result.code}.regex(new RegExp(${JSON.stringify(node.pattern)}))` }; }
744
+ catch { pushWarning('Unsupported constraint: pattern'); }
745
+ }
746
+ }
747
+ if (node.type === 'number' || node.type === 'integer') {
748
+ applyConstraint('minimum', node.minimum, isFiniteNumber(node.minimum), 'a finite number', (schema, value) => schema.min(value), 'min');
749
+ applyConstraint('maximum', node.maximum, isFiniteNumber(node.maximum), 'a finite number', (schema, value) => schema.max(value), 'max');
750
+ for (const [name, keyword, fallback, method] of [['exclusiveMinimum', node.exclusiveMinimum, node.minimum, 'gt'], ['exclusiveMaximum', node.exclusiveMaximum, node.maximum, 'lt']] as const) {
751
+ if (keyword === undefined || keyword === false) continue;
752
+ const value = keyword === true ? fallback : keyword;
753
+ if (!isFiniteNumber(value)) { pushWarning(`Invalid ${name}: expected a finite number${keyword === true ? ' in the corresponding inclusive bound' : ''}`); continue; }
754
+ result = { schema: (result.schema as any)[method](value), code: `${result.code}.${method}(${JSON.stringify(value)})` };
755
+ }
756
+ }
757
+ if (node.type === 'array') {
758
+ if (arrayIsTuple) {
759
+ if (node.minItems !== undefined) {
760
+ if (!isNonNegativeInteger(node.minItems)) pushWarning('Invalid minItems: expected a non-negative integer');
761
+ else result = { schema: result.schema.refine((items: unknown) => Array.isArray(items) && items.length >= node.minItems), code: `${result.code}.refine((items) => items.length >= ${node.minItems})` };
762
+ }
763
+ if (node.maxItems !== undefined) {
764
+ if (!isNonNegativeInteger(node.maxItems)) pushWarning('Invalid maxItems: expected a non-negative integer');
765
+ else result = { schema: result.schema.refine((items: unknown) => Array.isArray(items) && items.length <= node.maxItems), code: `${result.code}.refine((items) => items.length <= ${node.maxItems})` };
766
+ }
767
+ } else {
768
+ applyConstraint('minItems', node.minItems, isNonNegativeInteger(node.minItems), 'a non-negative integer', (schema, value) => schema.min(value), 'min');
769
+ applyConstraint('maxItems', node.maxItems, isNonNegativeInteger(node.maxItems), 'a non-negative integer', (schema, value) => schema.max(value), 'max');
770
+ }
771
+ }
772
+ if (node.type === 'object') {
773
+ if (node.minProperties !== undefined) {
774
+ if (!isNonNegativeInteger(node.minProperties)) pushWarning('Invalid minProperties: expected a non-negative integer');
775
+ else result = { schema: result.schema.refine((value: any) => Object.keys(value).length >= node.minProperties), code: `${result.code}.refine((value) => Object.keys(value).length >= ${node.minProperties})` };
776
+ }
777
+ if (node.maxProperties !== undefined) {
778
+ if (!isNonNegativeInteger(node.maxProperties)) pushWarning('Invalid maxProperties: expected a non-negative integer');
779
+ else result = { schema: result.schema.refine((value: any) => Object.keys(value).length <= node.maxProperties), code: `${result.code}.refine((value) => Object.keys(value).length <= ${node.maxProperties})` };
780
+ }
781
+ }
782
+ if (node.multipleOf !== undefined && (node.type === 'number' || node.type === 'integer') && isFiniteNumber(node.multipleOf) && node.multipleOf > 0) {
783
+ const multiple = node.multipleOf;
784
+ result = { schema: (result.schema as any).multipleOf(multiple), code: `${result.code}.multipleOf(${multiple})` };
785
+ } else if (node.multipleOf !== undefined && (node.type === 'number' || node.type === 'integer')) pushWarning('Invalid multipleOf: expected a positive number');
786
+ if (node.contains === undefined && (node.minContains !== undefined || node.maxContains !== undefined)) pushWarning('minContains/maxContains require contains and were ignored');
787
+ if (node.additionalItems !== undefined && !Array.isArray(node.items) && !Array.isArray(node.prefixItems)) pushWarning('additionalItems applies only to tuple schemas and was ignored');
788
+ if (node.type === 'object' && node.propertyNames !== undefined && !isNativeRecordObject(node)) {
789
+ if (typeof node.propertyNames === 'boolean' || (node.propertyNames !== null && typeof node.propertyNames === 'object' && !Array.isArray(node.propertyNames))) {
790
+ const propertyDefinition = typeof node.propertyNames === 'object' ? node.propertyNames as Record<string, unknown> : undefined;
791
+ const propertySchema = convert((propertyDefinition?.type === undefined && propertyDefinition && ('pattern' in propertyDefinition || 'minLength' in propertyDefinition || 'maxLength' in propertyDefinition)) ? { ...propertyDefinition, type: 'string' } as JsonSchema : node.propertyNames as JsonSchema, resolving, pointer(at, 'propertyNames'));
792
+ result = { schema: result.schema.refine((value: any) => Object.keys(value).every((key) => propertySchema.schema.safeParse(key).success)), code: `${result.code}.refine((value) => Object.keys(value).every((key) => (${propertySchema.code}).safeParse(key).success))` };
793
+ } else pushWarning('Invalid propertyNames: expected a schema');
794
+ }
795
+ if (node.type === 'object' && node.dependencies !== undefined) {
796
+ if (!node.dependencies || typeof node.dependencies !== 'object' || Array.isArray(node.dependencies)) pushWarning('Invalid dependencies: expected an object');
797
+ else for (const [key, dependency] of sortedEntries(node.dependencies as Record<string, unknown>)) {
798
+ if (Array.isArray(dependency)) {
799
+ if (!dependency.every((item) => typeof item === 'string')) { pushWarning('Invalid dependencies entry: expected a string array or schema'); continue; }
800
+ const requiredKeys = dependency as string[];
801
+ if (requiredKeys.length) result = {
802
+ schema: result.schema.refine((value: any) => !Object.prototype.hasOwnProperty.call(value, key) || requiredKeys.every((requiredKey) => Object.prototype.hasOwnProperty.call(value, requiredKey))),
803
+ code: `${result.code}.refine((value) => !Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(key)}) || ${JSON.stringify(requiredKeys)}.every((requiredKey) => Object.prototype.hasOwnProperty.call(value, requiredKey)))`,
804
+ };
805
+ } else if (isSchema(dependency)) {
806
+ const dependent = convert(dependency, resolving, pointer(pointer(at, 'dependencies'), key));
807
+ result = {
808
+ schema: result.schema.refine((value: any) => !Object.prototype.hasOwnProperty.call(value, key) || dependent.schema.safeParse(value).success),
809
+ code: `${result.code}.refine((value) => !Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(key)}) || (${dependent.code}).safeParse(value).success)`,
810
+ };
811
+ } else pushWarning('Invalid dependencies entry: expected a string array or schema');
812
+ }
813
+ }
814
+ if (node.type === 'object' && node.dependentRequired !== undefined && (!node.dependentRequired || typeof node.dependentRequired !== 'object' || Array.isArray(node.dependentRequired))) pushWarning('Invalid dependentRequired: expected an object of string arrays');
815
+ if (node.type === 'object' && node.dependentRequired && typeof node.dependentRequired === 'object' && !Array.isArray(node.dependentRequired)) {
816
+ const dependencies = sortedEntries(node.dependentRequired as Record<string, unknown>).filter(([, value]) => {
817
+ const valid = Array.isArray(value) && value.every((item) => typeof item === 'string');
818
+ if (!valid) pushWarning('Invalid dependentRequired entry: expected an array of strings');
819
+ return valid;
820
+ });
821
+ for (const [key, requiredKeys] of dependencies) {
822
+ const keys = requiredKeys as string[];
823
+ if (!keys.length) continue;
824
+ result = { schema: result.schema.refine((value: any) => !Object.prototype.hasOwnProperty.call(value, key) || keys.every((requiredKey) => Object.prototype.hasOwnProperty.call(value, requiredKey))), code: `${result.code}.refine((value) => !Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(key)}) || ${JSON.stringify(keys)}.every((requiredKey) => Object.prototype.hasOwnProperty.call(value, requiredKey)))` };
825
+ }
826
+ }
827
+ if (node.type === 'object' && node.dependentSchemas !== undefined && (!node.dependentSchemas || typeof node.dependentSchemas !== 'object' || Array.isArray(node.dependentSchemas))) pushWarning('Invalid dependentSchemas: expected an object of schemas');
828
+ if (node.type === 'object' && node.dependentSchemas && typeof node.dependentSchemas === 'object' && !Array.isArray(node.dependentSchemas)) {
829
+ for (const [key, dependency] of sortedEntries(node.dependentSchemas as Record<string, JsonSchema>)) {
830
+ const dependent = convert(dependency, resolving, pointer(pointer(at, 'dependentSchemas'), key));
831
+ result = { schema: result.schema.refine((value: any) => !Object.prototype.hasOwnProperty.call(value, key) || dependent.schema.safeParse(value).success), code: `${result.code}.refine((value) => !Object.prototype.hasOwnProperty.call(value, ${JSON.stringify(key)}) || (${dependent.code}).safeParse(value).success)` };
832
+ }
833
+ }
834
+ if (node.type === 'array' && node.contains !== undefined) {
835
+ if (isSchema(node.contains)) {
836
+ const contained = convert(node.contains, resolving, pointer(at, 'contains'));
837
+ const minValid = node.minContains === undefined || isNonNegativeInteger(node.minContains);
838
+ const maxValid = node.maxContains === undefined || isNonNegativeInteger(node.maxContains);
839
+ if (!minValid) pushWarning('Invalid minContains: expected a non-negative integer');
840
+ if (!maxValid) pushWarning('Invalid maxContains: expected a non-negative integer');
841
+ const min = minValid ? node.minContains ?? 1 : 1; const max = maxValid ? node.maxContains : undefined;
842
+ result = { schema: result.schema.refine((items: any) => { const count = items.filter((item: any) => contained.schema.safeParse(item).success).length; return count >= min && (max === undefined || count <= max); }), code: `${result.code}.refine((items) => { const count = items.filter((item) => ${contained.code}.safeParse(item).success).length; return count >= ${min}${max === undefined ? '' : ` && count <= ${max}`}; })` };
843
+ } else pushWarning('Invalid contains: expected a schema');
844
+ }
845
+ const hasIf = Object.prototype.hasOwnProperty.call(node, 'if');
846
+ if (!hasIf && (Object.prototype.hasOwnProperty.call(node, 'then') || Object.prototype.hasOwnProperty.call(node, 'else'))) pushWarning('then/else require if and were ignored');
847
+ if (hasIf) {
848
+ if (!isSchema(node.if)) pushWarning('Invalid if: expected a schema');
849
+ else {
850
+ const contextualize = (schema: JsonSchema): JsonSchema => {
851
+ if (typeof schema === 'boolean' || schema.type !== undefined) return schema;
852
+ return typeof node.type === 'string' ? { ...schema, type: node.type } : schema;
853
+ };
854
+ const condition = convert(contextualize(node.if), resolving, pointer(at, 'if'));
855
+ let whenTrue: { schema: z.ZodType; code: string } | undefined;
856
+ let whenFalse: { schema: z.ZodType; code: string } | undefined;
857
+ if (Object.prototype.hasOwnProperty.call(node, 'then')) {
858
+ if (!isSchema(node.then)) pushWarning('Invalid then: expected a schema');
859
+ else whenTrue = convert(contextualize(node.then), resolving, pointer(at, 'then'));
860
+ }
861
+ if (Object.prototype.hasOwnProperty.call(node, 'else')) {
862
+ if (!isSchema(node.else)) pushWarning('Invalid else: expected a schema');
863
+ else whenFalse = convert(contextualize(node.else), resolving, pointer(at, 'else'));
864
+ }
865
+ if (whenTrue || whenFalse) {
866
+ result = {
867
+ schema: result.schema.refine((value: unknown) => condition.schema.safeParse(value).success ? !whenTrue || whenTrue.schema.safeParse(value).success : !whenFalse || whenFalse.schema.safeParse(value).success),
868
+ code: `${result.code}.refine((value) => (${condition.code}).safeParse(value).success ? ${whenTrue ? `(${whenTrue.code}).safeParse(value).success` : 'true'} : ${whenFalse ? `(${whenFalse.code}).safeParse(value).success` : 'true'})`,
869
+ };
870
+ }
871
+ }
872
+ }
873
+ if (node.contentEncoding === 'base64' && node.type === 'string') result = { schema: (result.schema as any).base64(), code: `${result.code}.base64()` };
874
+ else if (node.contentEncoding === 'base64url' && node.type === 'string') result = { schema: (result.schema as any).base64url(), code: `${result.code}.base64url()` };
875
+ else if (node.contentEncoding === 'hex' && node.type === 'string') result = { schema: (result.schema as any).regex(/^(?:[0-9A-Fa-f]{2})*$/), code: `${result.code}.regex(/^(?:[0-9A-Fa-f]{2})*$/)` };
876
+ else if (node.contentEncoding !== undefined && node.type === 'string') pushWarning(`Unsupported contentEncoding: ${String(node.contentEncoding)}`);
877
+ else if (node.contentEncoding !== undefined && node.type !== 'string') pushWarning('Invalid contentEncoding: expected a string schema');
878
+ return result;
879
+ };
880
+
881
+ convert = (node: JsonSchema, resolving = new Set<string>(), at = '#'): Converted => {
882
+ const previousWarningAt = activeWarningAt;
883
+ activeWarningAt = at;
884
+ try {
885
+ const converted = convertCore(node, resolving, at);
886
+ if (typeof node === 'boolean' || !node || typeof node !== 'object' || Array.isArray(node)) return converted;
887
+ const metadata: Record<string, unknown> = {};
888
+ const addMetadata = (key: string, value: unknown): void => {
889
+ const literal = jsonLiteral(value);
890
+ if (literal === undefined) { pushWarning(`Invalid ${key}: expected a JSON value`); return; }
891
+ metadata[key] = JSON.parse(literal);
892
+ };
893
+ for (const key of ['title', 'description', 'examples', 'readOnly', 'writeOnly', 'deprecated', 'discriminator', 'xml', 'externalDocs'] as const) if (Object.prototype.hasOwnProperty.call(node, key)) addMetadata(key, node[key]);
894
+ for (const [key, value] of sortedEntries(node)) if (key.startsWith('x-')) addMetadata(key, value);
895
+ if (!Object.prototype.hasOwnProperty.call(node, 'examples') && Object.prototype.hasOwnProperty.call(node, 'example')) {
896
+ const literal = jsonLiteral(node.example);
897
+ if (literal === undefined) pushWarning('Invalid example: expected a JSON value');
898
+ else metadata.examples = [JSON.parse(literal)];
899
+ }
900
+ if (Object.keys(metadata).length === 0) return converted;
901
+ return { schema: converted.schema.meta(metadata), code: `${converted.code}.meta(${JSON.stringify(metadata)})` };
902
+ } finally { activeWarningAt = previousWarningAt; }
903
+ };
904
+
905
+ for (const entry of definitionEntries) definitionResults.set(entry.ref, convert(entry.schema, undefined, entry.ref));
906
+ rootResult = convert(source);
907
+
908
+ const details: AnalysisWarning[] = [...analysis.warnings];
909
+ for (const { message, at } of warningMessages) {
910
+ const code = warningCode(message);
911
+ if (code === 'ZOPIA_WARN_ONE_OF' && details.some((warning) => warning.code === code && warning.at === at)) continue;
912
+ if (code === 'ZOPIA_WARN_CUSTOM_FORMAT' && details.some((warning) => warning.code === code && warning.at === at)) continue;
913
+ const warning: AnalysisWarning = { code, at, keyword: warningKeyword(message), message };
914
+ if (!details.some((existing) => existing.code === warning.code && existing.at === warning.at && existing.message === warning.message)) details.push(warning);
915
+ }
916
+ const warnings = normalizeZopiaWarnings(details.map(({ keyword: _keyword, ...warning }) => warning));
917
+ const comments = warnings.map((warning) => {
918
+ const detail = details.find((candidate) => candidate.code === warning.code && candidate.at === warning.at && candidate.message.replace(/[\r\n\u2028\u2029]+/g, ' ').trim() === warning.message);
919
+ return formatZopiaWarningComment(warning, detail?.keyword ?? 'schema');
920
+ });
921
+ const expression = comments.length
922
+ ? `(\n${comments.map((comment) => ` ${comment}`).join('\n')}\n ${rootResult.code.replace(/\n/g, '\n ')}\n)`
923
+ : rootResult.code;
924
+ const declarations = definitionEntries.filter((entry) => usedDefinitionRefs.has(entry.ref)).map((entry) => `const ${entry.name} = ${definitionResults.get(entry.ref)!.code};`);
925
+ const documentation = typeof source === 'object'
926
+ ? [source.title, source.description].filter((value): value is string => typeof value === 'string' && value.length > 0).flatMap((value) => value.split(/\r?\n/))
927
+ : [];
928
+ const jsDoc = documentation.length ? `/**\n${documentation.map((line) => ` * ${line.replace(/\*\//g, '*\\/')}`).join('\n')}\n */\n` : '';
929
+ declarations.push(`${jsDoc}const ${rootName} = ${expression};`);
930
+ return { code: `${declarations.join('\n')}\n`, schema: rootResult.schema, warnings, overlays: analysis.overlays };
931
+ }