@takeshape/json-schema 11.52.0 → 11.55.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.
- package/dist/__tests__/schema-validator.test.d.ts +1 -0
- package/dist/__tests__/schema-validator.test.js +295 -0
- package/dist/converters/__tests__/schema-converter.test.d.ts +1 -0
- package/dist/converters/__tests__/schema-converter.test.js +1134 -0
- package/dist/converters/__tests__/search-shape-schema.json +495 -0
- package/dist/converters/index.d.ts +1 -2
- package/dist/converters/index.js +1 -16
- package/dist/converters/schema-converter.d.ts +0 -1
- package/dist/converters/schema-converter.js +540 -643
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -16
- package/dist/schema-validator.d.ts +1 -2
- package/dist/schema-validator.js +163 -189
- package/dist/utils/__tests__/references.test.d.ts +1 -0
- package/dist/utils/__tests__/references.test.js +121 -0
- package/dist/utils/__tests__/type-utils.test.d.ts +1 -0
- package/dist/utils/__tests__/type-utils.test.js +143 -0
- package/dist/utils/constants.d.ts +0 -1
- package/dist/utils/constants.js +49 -7
- package/dist/utils/index.d.ts +4 -5
- package/dist/utils/index.js +4 -49
- package/dist/utils/keys.d.ts +0 -1
- package/dist/utils/keys.js +5 -12
- package/dist/utils/references.d.ts +0 -1
- package/dist/utils/references.js +56 -57
- package/dist/utils/type-utils.d.ts +1 -2
- package/dist/utils/type-utils.js +36 -53
- package/dist/utils/types.d.ts +0 -1
- package/dist/utils/types.js +1 -5
- package/package.json +18 -25
- package/dist/converters/index.d.ts.map +0 -1
- package/dist/converters/schema-converter.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/schema-validator.d.ts.map +0 -1
- package/dist/utils/constants.d.ts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/keys.d.ts.map +0 -1
- package/dist/utils/references.d.ts.map +0 -1
- package/dist/utils/type-utils.d.ts.map +0 -1
- package/dist/utils/types.d.ts.map +0 -1
- package/es/converters/index.js +0 -1
- package/es/converters/schema-converter.js +0 -654
- package/es/index.js +0 -1
- package/es/schema-validator.js +0 -207
- package/es/utils/constants.js +0 -1
- package/es/utils/index.js +0 -4
- package/es/utils/keys.js +0 -9
- package/es/utils/references.js +0 -66
- package/es/utils/type-utils.js +0 -36
- package/es/utils/types.js +0 -1
|
@@ -1,662 +1,559 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return SchemaProcessingMode;
|
|
24
|
-
}(SchemaProcessingMode || {});
|
|
25
|
-
class SchemaConverter {
|
|
26
|
-
warnings = [];
|
|
27
|
-
stats = {
|
|
28
|
-
propertiesCount: 0,
|
|
29
|
-
stringLength: 0,
|
|
30
|
-
enumValuesCount: 0
|
|
31
|
-
};
|
|
32
|
-
#schema;
|
|
33
|
-
#config;
|
|
34
|
-
#usedDefinitions = new Set();
|
|
35
|
-
#definitions = {};
|
|
36
|
-
#definitionsReferences = new Map();
|
|
37
|
-
static convert(schema, options = {}) {
|
|
38
|
-
return new SchemaConverter(schema, options).run();
|
|
39
|
-
}
|
|
40
|
-
constructor(originalSchema, options = {}) {
|
|
41
|
-
const {
|
|
42
|
-
removePropertyKeyPatterns,
|
|
43
|
-
target = SchemaConversionTarget.JSONSchema,
|
|
44
|
-
inlineDefinitions = false,
|
|
45
|
-
maxDepth = 5,
|
|
46
|
-
allowUnknownKeys = false
|
|
47
|
-
} = options;
|
|
48
|
-
this.#config = {
|
|
49
|
-
target,
|
|
50
|
-
maxDepth,
|
|
51
|
-
allowUnknownKeys,
|
|
52
|
-
removePropertyKeyPatterns: removePropertyKeyPatterns?.map(pattern => new _minimatch.Minimatch(pattern)) ?? [],
|
|
53
|
-
inlineDefinitions
|
|
1
|
+
import { assert, ensureArray } from '@takeshape/util';
|
|
2
|
+
import intersection from 'lodash/intersection.js';
|
|
3
|
+
import uniq from 'lodash/uniq.js';
|
|
4
|
+
import { Minimatch } from 'minimatch';
|
|
5
|
+
import { getReferenceMap, isAllOfSchema, isAnyOfSchema, isArraySchema, isEnumSchema, isListSchema, isObjectSchema, isOneOfSchema, isPropertySchema, isRefSchema, isTupleSchema, pickJSONSchema7 } from "../utils/index.js";
|
|
6
|
+
export var SchemaConversionTarget;
|
|
7
|
+
(function (SchemaConversionTarget) {
|
|
8
|
+
SchemaConversionTarget["OpenAI"] = "OPENAI";
|
|
9
|
+
SchemaConversionTarget["JSONSchema"] = "JSON_SCHEMA";
|
|
10
|
+
})(SchemaConversionTarget || (SchemaConversionTarget = {}));
|
|
11
|
+
var SchemaProcessingMode;
|
|
12
|
+
(function (SchemaProcessingMode) {
|
|
13
|
+
SchemaProcessingMode["Schema"] = "SCHEMA";
|
|
14
|
+
SchemaProcessingMode["Definition"] = "DEFINITION";
|
|
15
|
+
SchemaProcessingMode["Count"] = "COUNT";
|
|
16
|
+
})(SchemaProcessingMode || (SchemaProcessingMode = {}));
|
|
17
|
+
export class SchemaConverter {
|
|
18
|
+
warnings = [];
|
|
19
|
+
stats = {
|
|
20
|
+
propertiesCount: 0,
|
|
21
|
+
stringLength: 0,
|
|
22
|
+
enumValuesCount: 0
|
|
54
23
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
isRequired: true,
|
|
80
|
-
path: [],
|
|
81
|
-
depth: 0
|
|
82
|
-
}, this.#schema);
|
|
83
|
-
if (!inlineDefinitions && this.#usedDefinitions.size) {
|
|
84
|
-
const defs = this.getUsedDefinitions();
|
|
85
|
-
if (Object.keys(defs).length) {
|
|
86
|
-
result.$defs = defs;
|
|
87
|
-
this.countDefinitions(defs);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Check the total property count
|
|
92
|
-
if (target === SchemaConversionTarget.OpenAI && this.stats.propertiesCount > 100) {
|
|
93
|
-
this.warnings.push(`Schema has ${this.stats.propertiesCount} total object properties, exceeding the 100 limit`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check string length total
|
|
97
|
-
if (target === SchemaConversionTarget.OpenAI && this.stats.stringLength > 15000) {
|
|
98
|
-
this.warnings.push(`Total string length of property names, definition names, enum values exceeds the 15,000 limit (${this.stats.stringLength})`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Check enum values count
|
|
102
|
-
if (target === SchemaConversionTarget.OpenAI && this.stats.enumValuesCount > 500) {
|
|
103
|
-
this.warnings.push(`Schema has ${this.stats.enumValuesCount} enum values across all properties, exceeding the 500 limit`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Check if root is an object
|
|
107
|
-
if (target === SchemaConversionTarget.OpenAI && !(0, _utils.isPropertySchema)(result)) {
|
|
108
|
-
this.warnings.push('Root level must be an object type, wrapping in an object');
|
|
109
|
-
|
|
110
|
-
// Attempt to fix by wrapping in an object if possible
|
|
111
|
-
return {
|
|
112
|
-
schema: {
|
|
113
|
-
type: 'object',
|
|
114
|
-
properties: {
|
|
115
|
-
root: result
|
|
116
|
-
},
|
|
117
|
-
required: ['root'],
|
|
118
|
-
additionalProperties: false
|
|
119
|
-
},
|
|
120
|
-
warnings: this.warnings,
|
|
121
|
-
stats: this.stats
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
schema: result,
|
|
126
|
-
warnings: this.warnings,
|
|
127
|
-
stats: this.stats
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
processSchema(context, schema) {
|
|
131
|
-
(0, _util.assert)(typeof schema === 'object', 'Schema must be an object');
|
|
132
|
-
const {
|
|
133
|
-
depth,
|
|
134
|
-
path,
|
|
135
|
-
isRequired,
|
|
136
|
-
processingMode
|
|
137
|
-
} = context;
|
|
138
|
-
const {
|
|
139
|
-
target,
|
|
140
|
-
maxDepth
|
|
141
|
-
} = this.#config;
|
|
142
|
-
// Clone the schema to avoid modifying the original
|
|
143
|
-
const schemaCopy = {
|
|
144
|
-
...schema
|
|
145
|
-
};
|
|
146
|
-
const currentPath = path.join('.') || 'root';
|
|
147
|
-
|
|
148
|
-
// Check for unsupported keywords
|
|
149
|
-
this.checkUnsupportedKeywords(schemaCopy, currentPath);
|
|
150
|
-
|
|
151
|
-
// Check depth limit
|
|
152
|
-
if (depth > maxDepth) {
|
|
153
|
-
this.warnings.push(`Nesting depth exceeds limit (max 5) at ${currentPath}`);
|
|
154
|
-
// Simplify the schema at this level
|
|
155
|
-
return this.addNullable({
|
|
156
|
-
type: 'string',
|
|
157
|
-
description: 'Simplified due to excessive nesting'
|
|
158
|
-
}, isRequired);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Handle $ref
|
|
162
|
-
if ((0, _utils.isRefSchema)(schemaCopy)) {
|
|
163
|
-
return this.processRefSchema(context, schemaCopy);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Process specific types
|
|
167
|
-
if ((0, _utils.isObjectSchema)(schemaCopy) || (0, _utils.isPropertySchema)(schemaCopy)) {
|
|
168
|
-
return this.addNullable(this.processObjectSchema(context, schemaCopy), isRequired);
|
|
169
|
-
}
|
|
170
|
-
if ((0, _utils.isArraySchema)(schemaCopy)) {
|
|
171
|
-
return this.addNullable(this.processArraySchema(context, schemaCopy), isRequired);
|
|
172
|
-
}
|
|
173
|
-
if ((0, _utils.isAnyOfSchema)(schemaCopy)) {
|
|
174
|
-
return this.addNullable(this.processAnyOfSchema(context, schemaCopy), isRequired);
|
|
175
|
-
}
|
|
176
|
-
if ((0, _utils.isOneOfSchema)(schemaCopy)) {
|
|
177
|
-
if (target === SchemaConversionTarget.OpenAI) {
|
|
178
|
-
this.warnings.push(`oneOf at ${currentPath} converted to anyOf (OpenAI only supports anyOf)`);
|
|
179
|
-
const {
|
|
180
|
-
oneOf,
|
|
181
|
-
...rest
|
|
182
|
-
} = schemaCopy;
|
|
183
|
-
return this.addNullable(this.processAnyOfSchema(context, {
|
|
184
|
-
...rest,
|
|
185
|
-
anyOf: oneOf
|
|
186
|
-
}), isRequired);
|
|
187
|
-
}
|
|
188
|
-
return this.addNullable(this.processOneOfSchema(context, schemaCopy), isRequired);
|
|
189
|
-
}
|
|
190
|
-
if ((0, _utils.isAllOfSchema)(schemaCopy)) {
|
|
191
|
-
if (target === SchemaConversionTarget.OpenAI) {
|
|
192
|
-
this.warnings.push(`allOf at ${currentPath} is not directly supported, attempting to merge schemas`);
|
|
193
|
-
return this.addNullable(this.mergeAllOfSchema(context, schemaCopy), isRequired);
|
|
194
|
-
}
|
|
195
|
-
return this.addNullable(this.processAllOfSchema(context, schemaCopy), isRequired);
|
|
196
|
-
}
|
|
197
|
-
if ((0, _utils.isEnumSchema)(schemaCopy)) {
|
|
198
|
-
return this.addNullable(this.processEnumSchema(context, schemaCopy), isRequired);
|
|
199
|
-
}
|
|
200
|
-
if (target === SchemaConversionTarget.OpenAI && schemaCopy.default) {
|
|
201
|
-
const defaultText = `Default value: ${typeof schemaCopy.default === 'object' ? JSON.stringify(schemaCopy.default) : schemaCopy.default}`;
|
|
202
|
-
schemaCopy.description = schema.description ? `${schema.description} ${defaultText}` : defaultText;
|
|
203
|
-
// biome-ignore lint/performance/noDelete: don't want to leave cruft
|
|
204
|
-
delete schemaCopy.default;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Update string length total for const values
|
|
208
|
-
if (schemaCopy.const && typeof schemaCopy.const === 'string' && (processingMode === SchemaProcessingMode.Schema || processingMode === SchemaProcessingMode.Count)) {
|
|
209
|
-
this.stats.stringLength += schemaCopy.const.length;
|
|
210
|
-
}
|
|
211
|
-
const newSchema = this.initializeNewSchema(schemaCopy, {
|
|
212
|
-
type: schemaCopy.type
|
|
213
|
-
});
|
|
214
|
-
return this.addNullable(newSchema, isRequired);
|
|
215
|
-
}
|
|
216
|
-
processDefinitions(defs) {
|
|
217
|
-
return Object.entries(defs).reduce((acc, [defName, def]) => {
|
|
218
|
-
acc[defName] = this.processSchema({
|
|
219
|
-
processingMode: SchemaProcessingMode.Definition,
|
|
220
|
-
isRequired: true,
|
|
221
|
-
path: [],
|
|
222
|
-
depth: 0
|
|
223
|
-
}, def);
|
|
224
|
-
return acc;
|
|
225
|
-
}, {});
|
|
226
|
-
}
|
|
227
|
-
countDefinitions(defs) {
|
|
228
|
-
for (const def of Object.values(defs)) {
|
|
229
|
-
this.processSchema({
|
|
230
|
-
processingMode: SchemaProcessingMode.Count,
|
|
231
|
-
isRequired: true,
|
|
232
|
-
path: [],
|
|
233
|
-
depth: 0
|
|
234
|
-
}, def);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
getUsedDefinitions() {
|
|
238
|
-
const defs = {};
|
|
239
|
-
for (const usedDef of this.#usedDefinitions) {
|
|
240
|
-
this.addDefinition(defs, usedDef);
|
|
241
|
-
|
|
242
|
-
// Add any definitions that are referenced by the used definition
|
|
243
|
-
const defRefs = this.#definitionsReferences.get(usedDef);
|
|
244
|
-
if (defRefs) {
|
|
245
|
-
for (const ref of defRefs) {
|
|
246
|
-
this.addDefinition(defs, ref);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return defs;
|
|
251
|
-
}
|
|
252
|
-
addDefinition(defs, key) {
|
|
253
|
-
const definition = this.#definitions[key];
|
|
254
|
-
if (definition) {
|
|
255
|
-
this.stats.stringLength += key.length;
|
|
256
|
-
defs[key] = definition;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
addNullable(schema, isRequired) {
|
|
260
|
-
if (this.#config.target === SchemaConversionTarget.JSONSchema) {
|
|
261
|
-
return schema;
|
|
262
|
-
}
|
|
263
|
-
if (isRequired) {
|
|
264
|
-
return schema;
|
|
24
|
+
#schema;
|
|
25
|
+
#config;
|
|
26
|
+
#usedDefinitions = new Set();
|
|
27
|
+
#definitions = {};
|
|
28
|
+
#definitionsReferences = new Map();
|
|
29
|
+
static convert(schema, options = {}) {
|
|
30
|
+
return new SchemaConverter(schema, options).run();
|
|
31
|
+
}
|
|
32
|
+
constructor(originalSchema, options = {}) {
|
|
33
|
+
const { removePropertyKeyPatterns, target = SchemaConversionTarget.JSONSchema, inlineDefinitions = false, maxDepth = 5, allowUnknownKeys = false } = options;
|
|
34
|
+
this.#config = {
|
|
35
|
+
target,
|
|
36
|
+
maxDepth,
|
|
37
|
+
allowUnknownKeys,
|
|
38
|
+
removePropertyKeyPatterns: removePropertyKeyPatterns?.map((pattern) => new Minimatch(pattern)) ?? [],
|
|
39
|
+
inlineDefinitions
|
|
40
|
+
};
|
|
41
|
+
const { $defs, definitions, ...schema } = originalSchema;
|
|
42
|
+
this.#schema = schema;
|
|
43
|
+
if ($defs || definitions) {
|
|
44
|
+
const defs = { ...$defs, ...definitions };
|
|
45
|
+
this.#definitionsReferences = getReferenceMap(defs);
|
|
46
|
+
this.#definitions = this.processDefinitions(defs);
|
|
47
|
+
}
|
|
265
48
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
49
|
+
run() {
|
|
50
|
+
const { target, inlineDefinitions } = this.#config;
|
|
51
|
+
// Process the schema
|
|
52
|
+
const result = this.processSchema({
|
|
53
|
+
processingMode: SchemaProcessingMode.Schema,
|
|
54
|
+
isRequired: true,
|
|
55
|
+
path: [],
|
|
56
|
+
depth: 0
|
|
57
|
+
}, this.#schema);
|
|
58
|
+
if (!inlineDefinitions && this.#usedDefinitions.size) {
|
|
59
|
+
const defs = this.getUsedDefinitions();
|
|
60
|
+
if (Object.keys(defs).length) {
|
|
61
|
+
result.$defs = defs;
|
|
62
|
+
this.countDefinitions(defs);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Check the total property count
|
|
66
|
+
if (target === SchemaConversionTarget.OpenAI && this.stats.propertiesCount > 100) {
|
|
67
|
+
this.warnings.push(`Schema has ${this.stats.propertiesCount} total object properties, exceeding the 100 limit`);
|
|
68
|
+
}
|
|
69
|
+
// Check string length total
|
|
70
|
+
if (target === SchemaConversionTarget.OpenAI && this.stats.stringLength > 15000) {
|
|
71
|
+
this.warnings.push(`Total string length of property names, definition names, enum values exceeds the 15,000 limit (${this.stats.stringLength})`);
|
|
72
|
+
}
|
|
73
|
+
// Check enum values count
|
|
74
|
+
if (target === SchemaConversionTarget.OpenAI && this.stats.enumValuesCount > 500) {
|
|
75
|
+
this.warnings.push(`Schema has ${this.stats.enumValuesCount} enum values across all properties, exceeding the 500 limit`);
|
|
76
|
+
}
|
|
77
|
+
// Check if root is an object
|
|
78
|
+
if (target === SchemaConversionTarget.OpenAI && !isPropertySchema(result)) {
|
|
79
|
+
this.warnings.push('Root level must be an object type, wrapping in an object');
|
|
80
|
+
// Attempt to fix by wrapping in an object if possible
|
|
81
|
+
return {
|
|
82
|
+
schema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
root: result
|
|
86
|
+
},
|
|
87
|
+
required: ['root'],
|
|
88
|
+
additionalProperties: false
|
|
89
|
+
},
|
|
90
|
+
warnings: this.warnings,
|
|
91
|
+
stats: this.stats
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
schema: result,
|
|
96
|
+
warnings: this.warnings,
|
|
97
|
+
stats: this.stats
|
|
98
|
+
};
|
|
271
99
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
100
|
+
processSchema(context, schema) {
|
|
101
|
+
assert(typeof schema === 'object', 'Schema must be an object');
|
|
102
|
+
const { depth, path, isRequired, processingMode } = context;
|
|
103
|
+
const { target, maxDepth } = this.#config;
|
|
104
|
+
// Clone the schema to avoid modifying the original
|
|
105
|
+
const schemaCopy = { ...schema };
|
|
106
|
+
const currentPath = path.join('.') || 'root';
|
|
107
|
+
// Check for unsupported keywords
|
|
108
|
+
this.checkUnsupportedKeywords(schemaCopy, currentPath);
|
|
109
|
+
// Check depth limit
|
|
110
|
+
if (depth > maxDepth) {
|
|
111
|
+
this.warnings.push(`Nesting depth exceeds limit (max 5) at ${currentPath}`);
|
|
112
|
+
// Simplify the schema at this level
|
|
113
|
+
return this.addNullable({ type: 'string', description: 'Simplified due to excessive nesting' }, isRequired);
|
|
114
|
+
}
|
|
115
|
+
// Handle $ref
|
|
116
|
+
if (isRefSchema(schemaCopy)) {
|
|
117
|
+
return this.processRefSchema(context, schemaCopy);
|
|
118
|
+
}
|
|
119
|
+
// Process specific types
|
|
120
|
+
if (isObjectSchema(schemaCopy) || isPropertySchema(schemaCopy)) {
|
|
121
|
+
return this.addNullable(this.processObjectSchema(context, schemaCopy), isRequired);
|
|
122
|
+
}
|
|
123
|
+
if (isArraySchema(schemaCopy)) {
|
|
124
|
+
return this.addNullable(this.processArraySchema(context, schemaCopy), isRequired);
|
|
125
|
+
}
|
|
126
|
+
if (isAnyOfSchema(schemaCopy)) {
|
|
127
|
+
return this.addNullable(this.processAnyOfSchema(context, schemaCopy), isRequired);
|
|
128
|
+
}
|
|
129
|
+
if (isOneOfSchema(schemaCopy)) {
|
|
130
|
+
if (target === SchemaConversionTarget.OpenAI) {
|
|
131
|
+
this.warnings.push(`oneOf at ${currentPath} converted to anyOf (OpenAI only supports anyOf)`);
|
|
132
|
+
const { oneOf, ...rest } = schemaCopy;
|
|
133
|
+
return this.addNullable(this.processAnyOfSchema(context, { ...rest, anyOf: oneOf }), isRequired);
|
|
134
|
+
}
|
|
135
|
+
return this.addNullable(this.processOneOfSchema(context, schemaCopy), isRequired);
|
|
136
|
+
}
|
|
137
|
+
if (isAllOfSchema(schemaCopy)) {
|
|
138
|
+
if (target === SchemaConversionTarget.OpenAI) {
|
|
139
|
+
this.warnings.push(`allOf at ${currentPath} is not directly supported, attempting to merge schemas`);
|
|
140
|
+
return this.addNullable(this.mergeAllOfSchema(context, schemaCopy), isRequired);
|
|
141
|
+
}
|
|
142
|
+
return this.addNullable(this.processAllOfSchema(context, schemaCopy), isRequired);
|
|
143
|
+
}
|
|
144
|
+
if (isEnumSchema(schemaCopy)) {
|
|
145
|
+
return this.addNullable(this.processEnumSchema(context, schemaCopy), isRequired);
|
|
146
|
+
}
|
|
147
|
+
if (target === SchemaConversionTarget.OpenAI && schemaCopy.default) {
|
|
148
|
+
const defaultText = `Default value: ${typeof schemaCopy.default === 'object' ? JSON.stringify(schemaCopy.default) : schemaCopy.default}`;
|
|
149
|
+
schemaCopy.description = schema.description ? `${schema.description} ${defaultText}` : defaultText;
|
|
150
|
+
// biome-ignore lint/performance/noDelete: don't want to leave cruft
|
|
151
|
+
delete schemaCopy.default;
|
|
152
|
+
}
|
|
153
|
+
// Update string length total for const values
|
|
154
|
+
if (schemaCopy.const &&
|
|
155
|
+
typeof schemaCopy.const === 'string' &&
|
|
156
|
+
(processingMode === SchemaProcessingMode.Schema || processingMode === SchemaProcessingMode.Count)) {
|
|
157
|
+
this.stats.stringLength += schemaCopy.const.length;
|
|
158
|
+
}
|
|
159
|
+
const newSchema = this.initializeNewSchema(schemaCopy, {
|
|
160
|
+
type: schemaCopy.type
|
|
161
|
+
});
|
|
162
|
+
return this.addNullable(newSchema, isRequired);
|
|
163
|
+
}
|
|
164
|
+
processDefinitions(defs) {
|
|
165
|
+
return Object.entries(defs).reduce((acc, [defName, def]) => {
|
|
166
|
+
acc[defName] = this.processSchema({
|
|
167
|
+
processingMode: SchemaProcessingMode.Definition,
|
|
168
|
+
isRequired: true,
|
|
169
|
+
path: [],
|
|
170
|
+
depth: 0
|
|
171
|
+
}, def);
|
|
172
|
+
return acc;
|
|
173
|
+
}, {});
|
|
174
|
+
}
|
|
175
|
+
countDefinitions(defs) {
|
|
176
|
+
for (const def of Object.values(defs)) {
|
|
177
|
+
this.processSchema({
|
|
178
|
+
processingMode: SchemaProcessingMode.Count,
|
|
179
|
+
isRequired: true,
|
|
180
|
+
path: [],
|
|
181
|
+
depth: 0
|
|
182
|
+
}, def);
|
|
183
|
+
}
|
|
290
184
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
185
|
+
getUsedDefinitions() {
|
|
186
|
+
const defs = {};
|
|
187
|
+
for (const usedDef of this.#usedDefinitions) {
|
|
188
|
+
this.addDefinition(defs, usedDef);
|
|
189
|
+
// Add any definitions that are referenced by the used definition
|
|
190
|
+
const defRefs = this.#definitionsReferences.get(usedDef);
|
|
191
|
+
if (defRefs) {
|
|
192
|
+
for (const ref of defRefs) {
|
|
193
|
+
this.addDefinition(defs, ref);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return defs;
|
|
294
198
|
}
|
|
295
|
-
|
|
296
|
-
|
|
199
|
+
addDefinition(defs, key) {
|
|
200
|
+
const definition = this.#definitions[key];
|
|
201
|
+
if (definition) {
|
|
202
|
+
this.stats.stringLength += key.length;
|
|
203
|
+
defs[key] = definition;
|
|
204
|
+
}
|
|
297
205
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
206
|
+
addNullable(schema, isRequired) {
|
|
207
|
+
if (this.#config.target === SchemaConversionTarget.JSONSchema) {
|
|
208
|
+
return schema;
|
|
209
|
+
}
|
|
210
|
+
if (isRequired) {
|
|
211
|
+
return schema;
|
|
212
|
+
}
|
|
213
|
+
if (schema.type && !isObjectSchema(schema) && !isArraySchema(schema)) {
|
|
214
|
+
return {
|
|
215
|
+
...schema,
|
|
216
|
+
type: uniq([...ensureArray(schema.type), 'null'])
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
anyOf: [...(schema.anyOf ? schema.anyOf : [schema]), { type: 'null' }]
|
|
312
221
|
};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
222
|
+
}
|
|
223
|
+
processRefSchema(context, schema) {
|
|
224
|
+
const { isRequired, processingMode } = context;
|
|
225
|
+
const newSchema = {
|
|
226
|
+
$ref: schema.$ref
|
|
317
227
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
228
|
+
// Normalize definitions path
|
|
229
|
+
if (newSchema.$ref.startsWith('#/definitions/')) {
|
|
230
|
+
newSchema.$ref = newSchema.$ref.replace('#/definitions/', '#/$defs/');
|
|
231
|
+
}
|
|
232
|
+
const defName = newSchema.$ref.replace('#/$defs/', '');
|
|
233
|
+
if (defName && this.#config.inlineDefinitions && this.#definitions[defName]) {
|
|
234
|
+
return this.processSchema(context, this.#definitions[defName]);
|
|
235
|
+
}
|
|
236
|
+
if (processingMode === SchemaProcessingMode.Schema) {
|
|
237
|
+
this.#usedDefinitions.add(defName);
|
|
238
|
+
}
|
|
239
|
+
return this.addNullable(newSchema, isRequired);
|
|
240
|
+
}
|
|
241
|
+
initializeNewSchema(schema, newSchema) {
|
|
242
|
+
// OpenAI will only allow some very specific properties, so start fairly clean
|
|
243
|
+
if (this.#config.target === SchemaConversionTarget.OpenAI) {
|
|
244
|
+
let base = {
|
|
245
|
+
title: schema.title,
|
|
246
|
+
description: schema.description
|
|
247
|
+
};
|
|
248
|
+
if (newSchema.type === 'object') {
|
|
249
|
+
base = {
|
|
250
|
+
...base,
|
|
251
|
+
required: Object.keys(newSchema.properties ?? schema.properties ?? {}),
|
|
252
|
+
additionalProperties: false
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
else if (newSchema.type === 'array') {
|
|
256
|
+
base = {
|
|
257
|
+
...base,
|
|
258
|
+
items: []
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
else if (newSchema.type === 'number' ||
|
|
262
|
+
newSchema.type === 'integer' ||
|
|
263
|
+
newSchema.type === 'string' ||
|
|
264
|
+
newSchema.type === 'boolean') {
|
|
265
|
+
base = {
|
|
266
|
+
...base,
|
|
267
|
+
const: schema.const
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
...base,
|
|
272
|
+
...newSchema
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
// We may be converting a schema with non-spec properties
|
|
277
|
+
...(this.#config.allowUnknownKeys ? schema : pickJSONSchema7(schema)),
|
|
278
|
+
...newSchema
|
|
322
279
|
};
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
...base,
|
|
326
|
-
...newSchema
|
|
327
|
-
};
|
|
328
280
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
newSchemaProperties[key] = this.processSchema({
|
|
360
|
-
...context,
|
|
361
|
-
path: [...path, key],
|
|
362
|
-
depth: depth + 1,
|
|
363
|
-
isRequired: uniqueRequired.has(key)
|
|
364
|
-
}, property);
|
|
365
|
-
|
|
366
|
-
// Update string length total for property names
|
|
281
|
+
processObjectSchema(context, schema) {
|
|
282
|
+
const { depth, path, processingMode } = context;
|
|
283
|
+
const { target, removePropertyKeyPatterns } = this.#config;
|
|
284
|
+
const newSchema = this.initializeNewSchema(schema, {
|
|
285
|
+
type: 'object'
|
|
286
|
+
});
|
|
287
|
+
const uniqueRequired = new Set(schema.required ?? []);
|
|
288
|
+
const newSchemaProperties = {};
|
|
289
|
+
// Process properties
|
|
290
|
+
if (schema.properties) {
|
|
291
|
+
// Process each property
|
|
292
|
+
for (const [key, property] of Object.entries(schema.properties)) {
|
|
293
|
+
// Skip properties that match removePropertyKeyPatterns
|
|
294
|
+
if (removePropertyKeyPatterns.length && removePropertyKeyPatterns.some((pattern) => pattern.match(key))) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
newSchemaProperties[key] = this.processSchema({
|
|
298
|
+
...context,
|
|
299
|
+
path: [...path, key],
|
|
300
|
+
depth: depth + 1,
|
|
301
|
+
isRequired: uniqueRequired.has(key)
|
|
302
|
+
}, property);
|
|
303
|
+
// Update string length total for property names
|
|
304
|
+
if (processingMode === SchemaProcessingMode.Schema || processingMode === SchemaProcessingMode.Count) {
|
|
305
|
+
this.stats.stringLength += key.length;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const newPropertyKeys = Object.keys(newSchemaProperties);
|
|
310
|
+
// Update total property count
|
|
367
311
|
if (processingMode === SchemaProcessingMode.Schema || processingMode === SchemaProcessingMode.Count) {
|
|
368
|
-
|
|
312
|
+
this.stats.propertiesCount += newPropertyKeys.length;
|
|
369
313
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
required = (0, _intersection.default)([...uniqueRequired], newPropertyKeys);
|
|
384
|
-
}
|
|
385
|
-
return {
|
|
386
|
-
...newSchema,
|
|
387
|
-
properties: newSchemaProperties,
|
|
388
|
-
// Make all properties required for openai target, for json-schema some may have been removed
|
|
389
|
-
required
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
processArraySchema(context, schema) {
|
|
393
|
-
const {
|
|
394
|
-
depth,
|
|
395
|
-
path
|
|
396
|
-
} = context;
|
|
397
|
-
const newSchema = this.initializeNewSchema(schema, {
|
|
398
|
-
type: 'array'
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// Process items schema
|
|
402
|
-
if (schema.items) {
|
|
403
|
-
if ((0, _utils.isTupleSchema)(schema)) {
|
|
404
|
-
newSchema.items = schema.items.map((item, index) => this.processSchema({
|
|
405
|
-
...context,
|
|
406
|
-
path: [...path, 'items', index],
|
|
407
|
-
depth: depth + 1
|
|
408
|
-
}, item));
|
|
409
|
-
}
|
|
410
|
-
if ((0, _utils.isListSchema)(schema)) {
|
|
411
|
-
newSchema.items = this.processSchema({
|
|
412
|
-
...context,
|
|
413
|
-
path: [...path, 'items'],
|
|
414
|
-
depth: depth + 1
|
|
415
|
-
}, schema.items);
|
|
416
|
-
}
|
|
417
|
-
} else {
|
|
418
|
-
this.warnings.push(`Array missing items definition at ${path.join('.') || 'root'}, defaulting to string items`);
|
|
419
|
-
newSchema.items = {
|
|
420
|
-
type: 'string'
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
for (const keyword of this.getUnsupportedArrayKeywords()) {
|
|
424
|
-
if (schema[keyword] !== undefined) {
|
|
425
|
-
this.warnings.push(`Unsupported array keyword "${keyword}" at ${path.join('.') || 'root'} will be ignored`);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return newSchema;
|
|
429
|
-
}
|
|
430
|
-
getUnsupportedArrayKeywords() {
|
|
431
|
-
if (this.#config.target === SchemaConversionTarget.OpenAI) {
|
|
432
|
-
return [
|
|
433
|
-
// 'unevaluatedItems',
|
|
434
|
-
'contains',
|
|
435
|
-
// 'minContains',
|
|
436
|
-
// 'maxContains',
|
|
437
|
-
'minItems', 'maxItems', 'uniqueItems'];
|
|
438
|
-
}
|
|
439
|
-
return [];
|
|
440
|
-
}
|
|
441
|
-
processOneOfSchema(context, schema) {
|
|
442
|
-
const {
|
|
443
|
-
depth,
|
|
444
|
-
path
|
|
445
|
-
} = context;
|
|
446
|
-
const newSchema = this.initializeNewSchema(schema, {
|
|
447
|
-
oneOf: []
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
// Process each oneOf schema
|
|
451
|
-
newSchema.oneOf = schema.oneOf.map((subSchema, index) => {
|
|
452
|
-
return this.processSchema({
|
|
453
|
-
...context,
|
|
454
|
-
path: [...path, 'oneOf', index],
|
|
455
|
-
depth: depth + 1
|
|
456
|
-
}, subSchema);
|
|
457
|
-
});
|
|
458
|
-
return newSchema;
|
|
459
|
-
}
|
|
460
|
-
processAnyOfSchema(context, schema) {
|
|
461
|
-
const {
|
|
462
|
-
target
|
|
463
|
-
} = this.#config;
|
|
464
|
-
const {
|
|
465
|
-
depth,
|
|
466
|
-
path
|
|
467
|
-
} = context;
|
|
468
|
-
const newSchema = this.initializeNewSchema(schema, {
|
|
469
|
-
anyOf: []
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// Check if this is at the root level
|
|
473
|
-
if (target === SchemaConversionTarget.OpenAI && path.length === 0) {
|
|
474
|
-
this.warnings.push('anyOf at root level is not supported by OpenAI Structured Outputs');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Process each anyOf schema
|
|
478
|
-
newSchema.anyOf = schema.anyOf.map((subSchema, index) => {
|
|
479
|
-
return this.processSchema({
|
|
480
|
-
...context,
|
|
481
|
-
path: [...path, 'anyOf', index],
|
|
482
|
-
depth: depth + 1
|
|
483
|
-
}, subSchema);
|
|
484
|
-
});
|
|
485
|
-
return newSchema;
|
|
486
|
-
}
|
|
487
|
-
processAllOfSchema(context, schema) {
|
|
488
|
-
const {
|
|
489
|
-
depth,
|
|
490
|
-
path
|
|
491
|
-
} = context;
|
|
492
|
-
const newSchema = this.initializeNewSchema(schema, {
|
|
493
|
-
allOf: []
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
// Process each anyOf schema
|
|
497
|
-
newSchema.allOf = schema.allOf.map((subSchema, index) => {
|
|
498
|
-
return this.processSchema({
|
|
499
|
-
...context,
|
|
500
|
-
path: [...path, 'allOf', index],
|
|
501
|
-
depth: depth + 1
|
|
502
|
-
}, subSchema);
|
|
503
|
-
});
|
|
504
|
-
return newSchema;
|
|
505
|
-
}
|
|
506
|
-
mergeAllOfSchema(context, schema) {
|
|
507
|
-
const {
|
|
508
|
-
depth,
|
|
509
|
-
path
|
|
510
|
-
} = context;
|
|
511
|
-
const newSchema = this.initializeNewSchema(schema, {
|
|
512
|
-
type: 'object'
|
|
513
|
-
});
|
|
514
|
-
const allOfSchemas = schema.allOf ?? [];
|
|
515
|
-
|
|
516
|
-
// Try to merge properties from all schemas
|
|
517
|
-
for (const [index, value] of allOfSchemas.entries()) {
|
|
518
|
-
const subSchema = this.processSchema({
|
|
519
|
-
...context,
|
|
520
|
-
path: [...path, 'allOf', index]
|
|
521
|
-
}, value);
|
|
522
|
-
if ((0, _utils.isObjectSchema)(subSchema) && (0, _utils.isPropertySchema)(subSchema)) {
|
|
523
|
-
// Merge properties
|
|
524
|
-
newSchema.properties = {
|
|
525
|
-
...newSchema.properties,
|
|
526
|
-
...subSchema.properties
|
|
314
|
+
let required = schema.required;
|
|
315
|
+
if (target === SchemaConversionTarget.OpenAI) {
|
|
316
|
+
// OpenAI requires all properties to be required
|
|
317
|
+
required = newPropertyKeys;
|
|
318
|
+
}
|
|
319
|
+
else if (required) {
|
|
320
|
+
required = intersection([...uniqueRequired], newPropertyKeys);
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
...newSchema,
|
|
324
|
+
properties: newSchemaProperties,
|
|
325
|
+
// Make all properties required for openai target, for json-schema some may have been removed
|
|
326
|
+
required
|
|
527
327
|
};
|
|
528
|
-
|
|
529
|
-
// Merge required fields
|
|
530
|
-
if (Array.isArray(subSchema.required)) {
|
|
531
|
-
newSchema.required = [...(newSchema.required ?? []), ...subSchema.required];
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Preserve description if we don't have one yet
|
|
535
|
-
if (!newSchema.description && subSchema.description) {
|
|
536
|
-
newSchema.description = subSchema.description;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Preserve title if we don't have one yet
|
|
540
|
-
if (!newSchema.title && subSchema.title) {
|
|
541
|
-
newSchema.title = subSchema.title;
|
|
542
|
-
}
|
|
543
|
-
} else {
|
|
544
|
-
this.warnings.push(`Non-object schema in allOf at ${path.join('.') || 'root'} cannot be merged properly`);
|
|
545
|
-
}
|
|
546
328
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (Array.isArray(newSchema.enum)) {
|
|
568
|
-
// Update enum values count
|
|
569
|
-
if (processingMode === SchemaProcessingMode.Schema || processingMode === SchemaProcessingMode.Count) {
|
|
570
|
-
this.stats.enumValuesCount += newSchema.enum.length;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Check enum size
|
|
574
|
-
if (target === SchemaConversionTarget.OpenAI && newSchema.enum.length > 500) {
|
|
575
|
-
this.warnings.push(`Enum at ${path.join('.') || 'root'} has ${newSchema.enum.length} values, exceeding the 500 limit`);
|
|
576
|
-
newSchema.enum = newSchema.enum.slice(0, 500);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Check string length for large enums
|
|
580
|
-
if (newSchema.type === 'string') {
|
|
581
|
-
let totalLength = 0;
|
|
582
|
-
for (const val of newSchema.enum) {
|
|
583
|
-
if (typeof val === 'string') {
|
|
584
|
-
totalLength += val.length;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
if (target === SchemaConversionTarget.OpenAI && totalLength > 7500) {
|
|
588
|
-
this.warnings.push(`Enum strings at ${path.join('.') || 'root'} exceed 7500 characters (${totalLength}) with ${newSchema.enum.length} values`);
|
|
589
|
-
|
|
590
|
-
// Truncate enum values to fit within limits
|
|
591
|
-
let currentLength = 0;
|
|
592
|
-
const truncatedEnum = [];
|
|
593
|
-
for (const val of newSchema.enum) {
|
|
594
|
-
if (typeof val === 'string') {
|
|
595
|
-
if (currentLength + val.length <= 7500) {
|
|
596
|
-
truncatedEnum.push(val);
|
|
597
|
-
currentLength += val.length;
|
|
598
|
-
} else {
|
|
599
|
-
break;
|
|
600
|
-
}
|
|
601
|
-
} else {
|
|
602
|
-
truncatedEnum.push(val);
|
|
329
|
+
processArraySchema(context, schema) {
|
|
330
|
+
const { depth, path } = context;
|
|
331
|
+
const newSchema = this.initializeNewSchema(schema, {
|
|
332
|
+
type: 'array'
|
|
333
|
+
});
|
|
334
|
+
// Process items schema
|
|
335
|
+
if (schema.items) {
|
|
336
|
+
if (isTupleSchema(schema)) {
|
|
337
|
+
newSchema.items = schema.items.map((item, index) => this.processSchema({
|
|
338
|
+
...context,
|
|
339
|
+
path: [...path, 'items', index],
|
|
340
|
+
depth: depth + 1
|
|
341
|
+
}, item));
|
|
342
|
+
}
|
|
343
|
+
if (isListSchema(schema)) {
|
|
344
|
+
newSchema.items = this.processSchema({
|
|
345
|
+
...context,
|
|
346
|
+
path: [...path, 'items'],
|
|
347
|
+
depth: depth + 1
|
|
348
|
+
}, schema.items);
|
|
603
349
|
}
|
|
604
|
-
}
|
|
605
|
-
newSchema.enum = truncatedEnum;
|
|
606
350
|
}
|
|
607
|
-
|
|
608
|
-
|
|
351
|
+
else {
|
|
352
|
+
this.warnings.push(`Array missing items definition at ${path.join('.') || 'root'}, defaulting to string items`);
|
|
353
|
+
newSchema.items = { type: 'string' };
|
|
354
|
+
}
|
|
355
|
+
for (const keyword of this.getUnsupportedArrayKeywords()) {
|
|
356
|
+
if (schema[keyword] !== undefined) {
|
|
357
|
+
this.warnings.push(`Unsupported array keyword "${keyword}" at ${path.join('.') || 'root'} will be ignored`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return newSchema;
|
|
361
|
+
}
|
|
362
|
+
getUnsupportedArrayKeywords() {
|
|
363
|
+
if (this.#config.target === SchemaConversionTarget.OpenAI) {
|
|
364
|
+
return [
|
|
365
|
+
// 'unevaluatedItems',
|
|
366
|
+
'contains',
|
|
367
|
+
// 'minContains',
|
|
368
|
+
// 'maxContains',
|
|
369
|
+
'minItems',
|
|
370
|
+
'maxItems',
|
|
371
|
+
'uniqueItems'
|
|
372
|
+
];
|
|
373
|
+
}
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
processOneOfSchema(context, schema) {
|
|
377
|
+
const { depth, path } = context;
|
|
378
|
+
const newSchema = this.initializeNewSchema(schema, {
|
|
379
|
+
oneOf: []
|
|
380
|
+
});
|
|
381
|
+
// Process each oneOf schema
|
|
382
|
+
newSchema.oneOf = schema.oneOf.map((subSchema, index) => {
|
|
383
|
+
return this.processSchema({ ...context, path: [...path, 'oneOf', index], depth: depth + 1 }, subSchema);
|
|
384
|
+
});
|
|
385
|
+
return newSchema;
|
|
386
|
+
}
|
|
387
|
+
processAnyOfSchema(context, schema) {
|
|
388
|
+
const { target } = this.#config;
|
|
389
|
+
const { depth, path } = context;
|
|
390
|
+
const newSchema = this.initializeNewSchema(schema, {
|
|
391
|
+
anyOf: []
|
|
392
|
+
});
|
|
393
|
+
// Check if this is at the root level
|
|
394
|
+
if (target === SchemaConversionTarget.OpenAI && path.length === 0) {
|
|
395
|
+
this.warnings.push('anyOf at root level is not supported by OpenAI Structured Outputs');
|
|
396
|
+
}
|
|
397
|
+
// Process each anyOf schema
|
|
398
|
+
newSchema.anyOf = schema.anyOf.map((subSchema, index) => {
|
|
399
|
+
return this.processSchema({ ...context, path: [...path, 'anyOf', index], depth: depth + 1 }, subSchema);
|
|
400
|
+
});
|
|
401
|
+
return newSchema;
|
|
402
|
+
}
|
|
403
|
+
processAllOfSchema(context, schema) {
|
|
404
|
+
const { depth, path } = context;
|
|
405
|
+
const newSchema = this.initializeNewSchema(schema, {
|
|
406
|
+
allOf: []
|
|
407
|
+
});
|
|
408
|
+
// Process each anyOf schema
|
|
409
|
+
newSchema.allOf = schema.allOf.map((subSchema, index) => {
|
|
410
|
+
return this.processSchema({ ...context, path: [...path, 'allOf', index], depth: depth + 1 }, subSchema);
|
|
411
|
+
});
|
|
412
|
+
return newSchema;
|
|
413
|
+
}
|
|
414
|
+
mergeAllOfSchema(context, schema) {
|
|
415
|
+
const { depth, path } = context;
|
|
416
|
+
const newSchema = this.initializeNewSchema(schema, {
|
|
417
|
+
type: 'object'
|
|
418
|
+
});
|
|
419
|
+
const allOfSchemas = schema.allOf ?? [];
|
|
420
|
+
// Try to merge properties from all schemas
|
|
421
|
+
for (const [index, value] of allOfSchemas.entries()) {
|
|
422
|
+
const subSchema = this.processSchema({ ...context, path: [...path, 'allOf', index] }, value);
|
|
423
|
+
if (isObjectSchema(subSchema) && isPropertySchema(subSchema)) {
|
|
424
|
+
// Merge properties
|
|
425
|
+
newSchema.properties = {
|
|
426
|
+
...newSchema.properties,
|
|
427
|
+
...subSchema.properties
|
|
428
|
+
};
|
|
429
|
+
// Merge required fields
|
|
430
|
+
if (Array.isArray(subSchema.required)) {
|
|
431
|
+
newSchema.required = [...(newSchema.required ?? []), ...subSchema.required];
|
|
432
|
+
}
|
|
433
|
+
// Preserve description if we don't have one yet
|
|
434
|
+
if (!newSchema.description && subSchema.description) {
|
|
435
|
+
newSchema.description = subSchema.description;
|
|
436
|
+
}
|
|
437
|
+
// Preserve title if we don't have one yet
|
|
438
|
+
if (!newSchema.title && subSchema.title) {
|
|
439
|
+
newSchema.title = subSchema.title;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
this.warnings.push(`Non-object schema in allOf at ${path.join('.') || 'root'} cannot be merged properly`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Remove duplicate required fields
|
|
447
|
+
newSchema.required = [...new Set(newSchema.required)];
|
|
448
|
+
return this.processObjectSchema({ ...context, depth: depth + 1 }, newSchema);
|
|
449
|
+
}
|
|
450
|
+
processEnumSchema(context, schema) {
|
|
451
|
+
const { target } = this.#config;
|
|
452
|
+
const { path, processingMode } = context;
|
|
453
|
+
const newSchema = this.initializeNewSchema(schema, {
|
|
454
|
+
enum: schema.enum,
|
|
455
|
+
type: schema.type
|
|
456
|
+
});
|
|
457
|
+
if (Array.isArray(newSchema.enum)) {
|
|
458
|
+
// Update enum values count
|
|
459
|
+
if (processingMode === SchemaProcessingMode.Schema || processingMode === SchemaProcessingMode.Count) {
|
|
460
|
+
this.stats.enumValuesCount += newSchema.enum.length;
|
|
461
|
+
}
|
|
462
|
+
// Check enum size
|
|
463
|
+
if (target === SchemaConversionTarget.OpenAI && newSchema.enum.length > 500) {
|
|
464
|
+
this.warnings.push(`Enum at ${path.join('.') || 'root'} has ${newSchema.enum.length} values, exceeding the 500 limit`);
|
|
465
|
+
newSchema.enum = newSchema.enum.slice(0, 500);
|
|
466
|
+
}
|
|
467
|
+
// Check string length for large enums
|
|
468
|
+
if (newSchema.type === 'string') {
|
|
469
|
+
let totalLength = 0;
|
|
470
|
+
for (const val of newSchema.enum) {
|
|
471
|
+
if (typeof val === 'string') {
|
|
472
|
+
totalLength += val.length;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (target === SchemaConversionTarget.OpenAI && totalLength > 7500) {
|
|
476
|
+
this.warnings.push(`Enum strings at ${path.join('.') || 'root'} exceed 7500 characters (${totalLength}) with ${newSchema.enum.length} values`);
|
|
477
|
+
// Truncate enum values to fit within limits
|
|
478
|
+
let currentLength = 0;
|
|
479
|
+
const truncatedEnum = [];
|
|
480
|
+
for (const val of newSchema.enum) {
|
|
481
|
+
if (typeof val === 'string') {
|
|
482
|
+
if (currentLength + val.length <= 7500) {
|
|
483
|
+
truncatedEnum.push(val);
|
|
484
|
+
currentLength += val.length;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
truncatedEnum.push(val);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
newSchema.enum = truncatedEnum;
|
|
495
|
+
}
|
|
496
|
+
if (processingMode === SchemaProcessingMode.Schema || processingMode === SchemaProcessingMode.Count) {
|
|
497
|
+
this.stats.stringLength += totalLength;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return newSchema;
|
|
502
|
+
}
|
|
503
|
+
checkUnsupportedKeywords(schema, path) {
|
|
504
|
+
if (this.#config.target === SchemaConversionTarget.OpenAI) {
|
|
505
|
+
// String-specific unsupported keywords
|
|
506
|
+
if (schema.type === 'string' ||
|
|
507
|
+
(!schema.type &&
|
|
508
|
+
(schema.minLength !== undefined ||
|
|
509
|
+
schema.maxLength !== undefined ||
|
|
510
|
+
schema.pattern !== undefined ||
|
|
511
|
+
schema.format !== undefined))) {
|
|
512
|
+
const unsupportedStringKeywords = ['minLength', 'maxLength', 'pattern', 'format'];
|
|
513
|
+
for (const keyword of unsupportedStringKeywords) {
|
|
514
|
+
if (schema[keyword] !== undefined) {
|
|
515
|
+
this.warnings.push(`Unsupported string keyword "${keyword}" at ${path || 'root'} will be ignored`);
|
|
516
|
+
delete schema[keyword];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Number-specific unsupported keywords
|
|
521
|
+
if (schema.type === 'number' ||
|
|
522
|
+
schema.type === 'integer' ||
|
|
523
|
+
(!schema.type &&
|
|
524
|
+
(schema.minimum !== undefined || schema.maximum !== undefined || schema.multipleOf !== undefined))) {
|
|
525
|
+
const unsupportedNumberKeywords = ['minimum', 'maximum', 'multipleOf'];
|
|
526
|
+
for (const keyword of unsupportedNumberKeywords) {
|
|
527
|
+
if (schema[keyword] !== undefined) {
|
|
528
|
+
this.warnings.push(`Unsupported number keyword "${keyword}" at ${path || 'root'} will be ignored`);
|
|
529
|
+
delete schema[keyword];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Object-specific unsupported keywords
|
|
534
|
+
if (schema.type === 'object' || (!schema.type && schema.properties !== undefined)) {
|
|
535
|
+
const unsupportedObjectKeywords = [
|
|
536
|
+
'patternProperties',
|
|
537
|
+
// 'unevaluatedProperties',
|
|
538
|
+
'propertyNames',
|
|
539
|
+
'minProperties',
|
|
540
|
+
'maxProperties'
|
|
541
|
+
];
|
|
542
|
+
for (const keyword of unsupportedObjectKeywords) {
|
|
543
|
+
if (schema[keyword] !== undefined) {
|
|
544
|
+
this.warnings.push(`Unsupported object keyword "${keyword}" at ${path || 'root'} will be ignored`);
|
|
545
|
+
delete schema[keyword];
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Check Draft 7 keywords that aren't supported in OpenAI schema
|
|
550
|
+
const draft7Keywords = ['if', 'then', 'else', 'not', 'dependencies'];
|
|
551
|
+
for (const keyword of draft7Keywords) {
|
|
552
|
+
if (schema[keyword] !== undefined) {
|
|
553
|
+
this.warnings.push(`JSON Schema Draft 7 keyword "${keyword}" at ${path || 'root'} is not supported and will be ignored`);
|
|
554
|
+
delete schema[keyword];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
609
557
|
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return newSchema;
|
|
613
|
-
}
|
|
614
|
-
checkUnsupportedKeywords(schema, path) {
|
|
615
|
-
if (this.#config.target === SchemaConversionTarget.OpenAI) {
|
|
616
|
-
// String-specific unsupported keywords
|
|
617
|
-
if (schema.type === 'string' || !schema.type && (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined || schema.format !== undefined)) {
|
|
618
|
-
const unsupportedStringKeywords = ['minLength', 'maxLength', 'pattern', 'format'];
|
|
619
|
-
for (const keyword of unsupportedStringKeywords) {
|
|
620
|
-
if (schema[keyword] !== undefined) {
|
|
621
|
-
this.warnings.push(`Unsupported string keyword "${keyword}" at ${path || 'root'} will be ignored`);
|
|
622
|
-
delete schema[keyword];
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Number-specific unsupported keywords
|
|
628
|
-
if (schema.type === 'number' || schema.type === 'integer' || !schema.type && (schema.minimum !== undefined || schema.maximum !== undefined || schema.multipleOf !== undefined)) {
|
|
629
|
-
const unsupportedNumberKeywords = ['minimum', 'maximum', 'multipleOf'];
|
|
630
|
-
for (const keyword of unsupportedNumberKeywords) {
|
|
631
|
-
if (schema[keyword] !== undefined) {
|
|
632
|
-
this.warnings.push(`Unsupported number keyword "${keyword}" at ${path || 'root'} will be ignored`);
|
|
633
|
-
delete schema[keyword];
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Object-specific unsupported keywords
|
|
639
|
-
if (schema.type === 'object' || !schema.type && schema.properties !== undefined) {
|
|
640
|
-
const unsupportedObjectKeywords = ['patternProperties',
|
|
641
|
-
// 'unevaluatedProperties',
|
|
642
|
-
'propertyNames', 'minProperties', 'maxProperties'];
|
|
643
|
-
for (const keyword of unsupportedObjectKeywords) {
|
|
644
|
-
if (schema[keyword] !== undefined) {
|
|
645
|
-
this.warnings.push(`Unsupported object keyword "${keyword}" at ${path || 'root'} will be ignored`);
|
|
646
|
-
delete schema[keyword];
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Check Draft 7 keywords that aren't supported in OpenAI schema
|
|
652
|
-
const draft7Keywords = ['if', 'then', 'else', 'not', 'dependencies'];
|
|
653
|
-
for (const keyword of draft7Keywords) {
|
|
654
|
-
if (schema[keyword] !== undefined) {
|
|
655
|
-
this.warnings.push(`JSON Schema Draft 7 keyword "${keyword}" at ${path || 'root'} is not supported and will be ignored`);
|
|
656
|
-
delete schema[keyword];
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
558
|
}
|
|
660
|
-
}
|
|
661
559
|
}
|
|
662
|
-
exports.SchemaConverter = SchemaConverter;
|