openai 6.48.0 → 6.49.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/CHANGELOG.md +32 -0
- package/azure.d.mts.map +1 -1
- package/azure.d.ts.map +1 -1
- package/azure.js +5 -1
- package/azure.js.map +1 -1
- package/azure.mjs +5 -1
- package/azure.mjs.map +1 -1
- package/helpers/standard-schema.d.mts +104 -0
- package/helpers/standard-schema.d.mts.map +1 -0
- package/helpers/standard-schema.d.ts +104 -0
- package/helpers/standard-schema.d.ts.map +1 -0
- package/helpers/standard-schema.js +320 -0
- package/helpers/standard-schema.js.map +1 -0
- package/helpers/standard-schema.mjs +314 -0
- package/helpers/standard-schema.mjs.map +1 -0
- package/helpers/zod.d.mts +26 -3
- package/helpers/zod.d.mts.map +1 -1
- package/helpers/zod.d.ts +26 -3
- package/helpers/zod.d.ts.map +1 -1
- package/helpers/zod.js +97 -7
- package/helpers/zod.js.map +1 -1
- package/helpers/zod.mjs +96 -7
- package/helpers/zod.mjs.map +1 -1
- package/lib/AbstractChatCompletionRunner.d.mts.map +1 -1
- package/lib/AbstractChatCompletionRunner.d.ts.map +1 -1
- package/lib/AbstractChatCompletionRunner.js +43 -1
- package/lib/AbstractChatCompletionRunner.js.map +1 -1
- package/lib/AbstractChatCompletionRunner.mjs +43 -1
- package/lib/AbstractChatCompletionRunner.mjs.map +1 -1
- package/lib/ChatCompletionStream.d.mts +2 -1
- package/lib/ChatCompletionStream.d.mts.map +1 -1
- package/lib/ChatCompletionStream.d.ts +2 -1
- package/lib/ChatCompletionStream.d.ts.map +1 -1
- package/lib/ChatCompletionStream.js +53 -14
- package/lib/ChatCompletionStream.js.map +1 -1
- package/lib/ChatCompletionStream.mjs +53 -14
- package/lib/ChatCompletionStream.mjs.map +1 -1
- package/lib/ResponsesParser.d.mts.map +1 -1
- package/lib/ResponsesParser.d.ts.map +1 -1
- package/lib/ResponsesParser.js +3 -5
- package/lib/ResponsesParser.js.map +1 -1
- package/lib/ResponsesParser.mjs +3 -5
- package/lib/ResponsesParser.mjs.map +1 -1
- package/lib/jsonschema.d.mts +3 -0
- package/lib/jsonschema.d.mts.map +1 -1
- package/lib/jsonschema.d.ts +3 -0
- package/lib/jsonschema.d.ts.map +1 -1
- package/lib/transform.d.mts +25 -1
- package/lib/transform.d.mts.map +1 -1
- package/lib/transform.d.ts +25 -1
- package/lib/transform.d.ts.map +1 -1
- package/lib/transform.js +1521 -82
- package/lib/transform.js.map +1 -1
- package/lib/transform.mjs +1515 -82
- package/lib/transform.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/admin/organization/index.d.mts +1 -0
- package/resources/admin/organization/index.d.mts.map +1 -1
- package/resources/admin/organization/index.d.ts +1 -0
- package/resources/admin/organization/index.d.ts.map +1 -1
- package/resources/admin/organization/index.js +3 -1
- package/resources/admin/organization/index.js.map +1 -1
- package/resources/admin/organization/index.mjs +1 -0
- package/resources/admin/organization/index.mjs.map +1 -1
- package/resources/admin/organization/organization.d.mts +4 -0
- package/resources/admin/organization/organization.d.mts.map +1 -1
- package/resources/admin/organization/organization.d.ts +4 -0
- package/resources/admin/organization/organization.d.ts.map +1 -1
- package/resources/admin/organization/organization.js +4 -0
- package/resources/admin/organization/organization.js.map +1 -1
- package/resources/admin/organization/organization.mjs +4 -0
- package/resources/admin/organization/organization.mjs.map +1 -1
- package/resources/admin/organization/projects/index.d.mts +1 -0
- package/resources/admin/organization/projects/index.d.mts.map +1 -1
- package/resources/admin/organization/projects/index.d.ts +1 -0
- package/resources/admin/organization/projects/index.d.ts.map +1 -1
- package/resources/admin/organization/projects/index.js +3 -1
- package/resources/admin/organization/projects/index.js.map +1 -1
- package/resources/admin/organization/projects/index.mjs +1 -0
- package/resources/admin/organization/projects/index.mjs.map +1 -1
- package/resources/admin/organization/projects/projects.d.mts +4 -0
- package/resources/admin/organization/projects/projects.d.mts.map +1 -1
- package/resources/admin/organization/projects/projects.d.ts +4 -0
- package/resources/admin/organization/projects/projects.d.ts.map +1 -1
- package/resources/admin/organization/projects/projects.js +4 -0
- package/resources/admin/organization/projects/projects.js.map +1 -1
- package/resources/admin/organization/projects/projects.mjs +4 -0
- package/resources/admin/organization/projects/projects.mjs.map +1 -1
- package/resources/admin/organization/projects/spend-limit.d.mts +115 -0
- package/resources/admin/organization/projects/spend-limit.d.mts.map +1 -0
- package/resources/admin/organization/projects/spend-limit.d.ts +115 -0
- package/resources/admin/organization/projects/spend-limit.d.ts.map +1 -0
- package/resources/admin/organization/projects/spend-limit.js +67 -0
- package/resources/admin/organization/projects/spend-limit.js.map +1 -0
- package/resources/admin/organization/projects/spend-limit.mjs +63 -0
- package/resources/admin/organization/projects/spend-limit.mjs.map +1 -0
- package/resources/admin/organization/spend-limit.d.mts +108 -0
- package/resources/admin/organization/spend-limit.d.mts.map +1 -0
- package/resources/admin/organization/spend-limit.d.ts +108 -0
- package/resources/admin/organization/spend-limit.d.ts.map +1 -0
- package/resources/admin/organization/spend-limit.js +59 -0
- package/resources/admin/organization/spend-limit.js.map +1 -0
- package/resources/admin/organization/spend-limit.mjs +55 -0
- package/resources/admin/organization/spend-limit.mjs.map +1 -0
- package/resources/beta/responses/input-tokens.d.mts +6 -2
- package/resources/beta/responses/input-tokens.d.mts.map +1 -1
- package/resources/beta/responses/input-tokens.d.ts +6 -2
- package/resources/beta/responses/input-tokens.d.ts.map +1 -1
- package/resources/beta/responses/responses.d.mts +21 -11
- package/resources/beta/responses/responses.d.mts.map +1 -1
- package/resources/beta/responses/responses.d.ts +21 -11
- package/resources/beta/responses/responses.d.ts.map +1 -1
- package/resources/beta/responses/responses.js.map +1 -1
- package/resources/beta/responses/responses.mjs.map +1 -1
- package/resources/chat/completions/completions.d.mts +4 -3
- package/resources/chat/completions/completions.d.mts.map +1 -1
- package/resources/chat/completions/completions.d.ts +4 -3
- package/resources/chat/completions/completions.d.ts.map +1 -1
- package/resources/chat/completions/completions.js.map +1 -1
- package/resources/chat/completions/completions.mjs.map +1 -1
- package/resources/responses/input-tokens.d.mts +2 -1
- package/resources/responses/input-tokens.d.mts.map +1 -1
- package/resources/responses/input-tokens.d.ts +2 -1
- package/resources/responses/input-tokens.d.ts.map +1 -1
- package/resources/responses/responses.d.mts +9 -8
- package/resources/responses/responses.d.mts.map +1 -1
- package/resources/responses/responses.d.ts +9 -8
- package/resources/responses/responses.d.ts.map +1 -1
- package/resources/responses/responses.js.map +1 -1
- package/resources/responses/responses.mjs.map +1 -1
- package/resources/shared.d.mts +4 -1
- package/resources/shared.d.mts.map +1 -1
- package/resources/shared.d.ts +4 -1
- package/resources/shared.d.ts.map +1 -1
- package/src/azure.ts +5 -1
- package/src/helpers/standard-schema.ts +570 -0
- package/src/helpers/zod.ts +158 -29
- package/src/lib/AbstractChatCompletionRunner.ts +43 -1
- package/src/lib/ChatCompletionStream.ts +56 -8
- package/src/lib/ResponsesParser.ts +7 -5
- package/src/lib/jsonschema.ts +3 -0
- package/src/lib/transform.ts +1930 -103
- package/src/resources/admin/organization/index.ts +6 -0
- package/src/resources/admin/organization/organization.ts +16 -0
- package/src/resources/admin/organization/projects/index.ts +6 -0
- package/src/resources/admin/organization/projects/projects.ts +16 -0
- package/src/resources/admin/organization/projects/spend-limit.ts +156 -0
- package/src/resources/admin/organization/spend-limit.ts +144 -0
- package/src/resources/beta/responses/input-tokens.ts +6 -2
- package/src/resources/beta/responses/responses.ts +21 -10
- package/src/resources/chat/completions/completions.ts +4 -3
- package/src/resources/responses/input-tokens.ts +2 -1
- package/src/resources/responses/responses.ts +9 -7
- package/src/resources/shared.ts +4 -1
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/lib/transform.ts
CHANGED
|
@@ -1,34 +1,639 @@
|
|
|
1
1
|
import type { JSONSchema, JSONSchemaDefinition } from './jsonschema';
|
|
2
2
|
|
|
3
|
+
const JSON_SCHEMA_ANNOTATION_KEYWORDS = new Set([
|
|
4
|
+
'$comment',
|
|
5
|
+
'default',
|
|
6
|
+
'description',
|
|
7
|
+
'examples',
|
|
8
|
+
'readOnly',
|
|
9
|
+
'title',
|
|
10
|
+
'writeOnly',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const JSON_SCHEMA_ROOT_METADATA_KEYWORDS = new Set(['$id', '$schema']);
|
|
14
|
+
|
|
15
|
+
const JSON_SCHEMA_OBJECT_KEYWORDS = new Set([
|
|
16
|
+
'additionalProperties',
|
|
17
|
+
'dependencies',
|
|
18
|
+
'maxProperties',
|
|
19
|
+
'minProperties',
|
|
20
|
+
'patternProperties',
|
|
21
|
+
'properties',
|
|
22
|
+
'propertyNames',
|
|
23
|
+
'required',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS = [
|
|
27
|
+
'additionalItems',
|
|
28
|
+
'additionalProperties',
|
|
29
|
+
'contains',
|
|
30
|
+
'contentSchema',
|
|
31
|
+
'else',
|
|
32
|
+
'if',
|
|
33
|
+
'not',
|
|
34
|
+
'propertyNames',
|
|
35
|
+
'then',
|
|
36
|
+
'unevaluatedItems',
|
|
37
|
+
'unevaluatedProperties',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS = ['allOf', 'anyOf', 'items', 'oneOf', 'prefixItems'];
|
|
41
|
+
|
|
42
|
+
const JSON_SCHEMA_MAP_SCHEMA_KEYWORDS = [
|
|
43
|
+
'$defs',
|
|
44
|
+
'definitions',
|
|
45
|
+
'dependentSchemas',
|
|
46
|
+
'dependencies',
|
|
47
|
+
'patternProperties',
|
|
48
|
+
'properties',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const JSON_SCHEMA_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
|
|
52
|
+
'$anchor',
|
|
53
|
+
'$dynamicAnchor',
|
|
54
|
+
'$dynamicRef',
|
|
55
|
+
'$recursiveAnchor',
|
|
56
|
+
'$recursiveRef',
|
|
57
|
+
'allOf',
|
|
58
|
+
'contains',
|
|
59
|
+
'contentEncoding',
|
|
60
|
+
'contentMediaType',
|
|
61
|
+
'contentSchema',
|
|
62
|
+
'dependentRequired',
|
|
63
|
+
'dependentSchemas',
|
|
64
|
+
'dependencies',
|
|
65
|
+
'else',
|
|
66
|
+
'if',
|
|
67
|
+
'maxContains',
|
|
68
|
+
'maxProperties',
|
|
69
|
+
'minContains',
|
|
70
|
+
'minProperties',
|
|
71
|
+
'not',
|
|
72
|
+
'patternProperties',
|
|
73
|
+
'prefixItems',
|
|
74
|
+
'propertyNames',
|
|
75
|
+
'then',
|
|
76
|
+
'unevaluatedItems',
|
|
77
|
+
'unevaluatedProperties',
|
|
78
|
+
'uniqueItems',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const MERGEABLE_OBJECT_ALL_OF_KEYWORDS = new Set([
|
|
82
|
+
...JSON_SCHEMA_ANNOTATION_KEYWORDS,
|
|
83
|
+
'additionalProperties',
|
|
84
|
+
'properties',
|
|
85
|
+
'required',
|
|
86
|
+
'type',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
type JSONSchemaChildVisitor = (schema: unknown, path: string[], keyword: string) => void;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Visits only values carried by JSON Schema keywords that contain schemas.
|
|
93
|
+
* Literal payloads such as enum, const, and default deliberately do not
|
|
94
|
+
* participate.
|
|
95
|
+
*/
|
|
96
|
+
export function forEachJSONSchemaChild(
|
|
97
|
+
schema: JSONSchema | Record<string, unknown>,
|
|
98
|
+
path: string[],
|
|
99
|
+
visit: JSONSchemaChildVisitor,
|
|
100
|
+
): void {
|
|
101
|
+
const record = schema as Record<string, unknown>;
|
|
102
|
+
|
|
103
|
+
for (const keyword of JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS) {
|
|
104
|
+
if (keyword in record) {
|
|
105
|
+
visit(record[keyword], [...path, keyword], keyword);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const keyword of JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS) {
|
|
110
|
+
const children = record[keyword];
|
|
111
|
+
if (Array.isArray(children)) {
|
|
112
|
+
for (const [index, child] of children.entries()) {
|
|
113
|
+
visit(child, [...path, keyword, String(index)], keyword);
|
|
114
|
+
}
|
|
115
|
+
} else if (children !== undefined) {
|
|
116
|
+
visit(children, [...path, keyword], keyword);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const keyword of JSON_SCHEMA_MAP_SCHEMA_KEYWORDS) {
|
|
121
|
+
const children = record[keyword];
|
|
122
|
+
if (!isObject(children)) continue;
|
|
123
|
+
|
|
124
|
+
for (const [key, child] of Object.entries(children)) {
|
|
125
|
+
// Draft 7 dependencies also permits property dependency arrays. They
|
|
126
|
+
// are not schemas and must not be traversed as literal JSON payloads.
|
|
127
|
+
if (keyword === 'dependencies' && !isSchemaDefinition(child)) continue;
|
|
128
|
+
visit(child, [...path, keyword, key], keyword);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
3
133
|
export function toStrictJsonSchema(schema: JSONSchema): JSONSchema {
|
|
4
|
-
|
|
134
|
+
const schemaCopy = structuredClone(schema);
|
|
135
|
+
// JSON serialization omits undefined object properties. Drop optional
|
|
136
|
+
// placeholders before any structural checks so they cannot accidentally
|
|
137
|
+
// look like validation-bearing schema keywords.
|
|
138
|
+
stripUndefinedSchemaKeywords(schemaCopy);
|
|
139
|
+
normalizeSingletonTypeArrays(schemaCopy);
|
|
140
|
+
// Root ref/allOf normalization can promote a nested branch into the root.
|
|
141
|
+
// Reject separate resource scopes while their original nesting is still
|
|
142
|
+
// visible so branch-local definitions cannot be rebound at the root.
|
|
143
|
+
assertNoNestedSchemaIds(schemaCopy);
|
|
144
|
+
normalizeRootRefAndAllOf(schemaCopy);
|
|
145
|
+
|
|
146
|
+
if (schemaCopy.type !== 'object') {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Root schema must have type: 'object' but got type: ${
|
|
149
|
+
schemaCopy.type ? `'${schemaCopy.type}'` : 'undefined'
|
|
150
|
+
}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (schemaCopy.anyOf !== undefined) {
|
|
5
154
|
throw new Error(
|
|
6
|
-
|
|
155
|
+
'Root schema must not use `anyOf` because strict Structured Outputs requires a root object without a union.',
|
|
7
156
|
);
|
|
8
157
|
}
|
|
9
158
|
|
|
10
|
-
|
|
11
|
-
|
|
159
|
+
validateRefSchemas(schemaCopy, [], schemaCopy);
|
|
160
|
+
preserveAllOfRefTargets(schemaCopy);
|
|
161
|
+
validateRefSchemas(schemaCopy, [], schemaCopy);
|
|
162
|
+
rewriteLocalRefsIntoFilteredAnyOfBranches(schemaCopy);
|
|
163
|
+
// Resolve representable object intersections before recursive
|
|
164
|
+
// strictification closes referenced definitions. Otherwise a definition
|
|
165
|
+
// reached through an allOf ref would be closed in isolation before its
|
|
166
|
+
// sibling object properties can be merged.
|
|
167
|
+
normalizeObjectAllOfBranches(schemaCopy, [], schemaCopy);
|
|
168
|
+
const strictSchema = ensureStrictJsonSchema(schemaCopy, [], schemaCopy);
|
|
169
|
+
validateRefSchemas(strictSchema, [], strictSchema);
|
|
170
|
+
return strictSchema;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function stripUndefinedSchemaKeywords(
|
|
174
|
+
schema: JSONSchemaDefinition,
|
|
175
|
+
visited: Set<JSONSchema> = new Set(),
|
|
176
|
+
): void {
|
|
177
|
+
if (typeof schema === 'boolean' || !isObject(schema) || visited.has(schema)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
visited.add(schema);
|
|
181
|
+
|
|
182
|
+
const schemaRecord = schema as Record<string, unknown>;
|
|
183
|
+
for (const keyword of Object.keys(schemaRecord)) {
|
|
184
|
+
if (schemaRecord[keyword] === undefined) {
|
|
185
|
+
delete schemaRecord[keyword];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
forEachJSONSchemaChild(schema, [], (child) => {
|
|
190
|
+
stripUndefinedSchemaKeywords(child as JSONSchemaDefinition, visited);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Root ref inlining and singleton allOf flattening can expose each other.
|
|
196
|
+
* Iterate until flattening no longer produces another root ref so every
|
|
197
|
+
* exactly representable chain reaches its final object form before the root
|
|
198
|
+
* type check runs.
|
|
199
|
+
*/
|
|
200
|
+
function normalizeRootRefAndAllOf(schema: JSONSchema): void {
|
|
201
|
+
const seenRefs = new Set<string>();
|
|
202
|
+
while (true) {
|
|
203
|
+
if (typeof schema.$ref === 'string') {
|
|
204
|
+
if (seenRefs.has(schema.$ref)) {
|
|
205
|
+
throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(schema.$ref));
|
|
206
|
+
}
|
|
207
|
+
seenRefs.add(schema.$ref);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
inlineRootRefObject(schema);
|
|
211
|
+
preserveAllOfRefTargets(schema, true);
|
|
212
|
+
normalizeRootAllOf(schema);
|
|
213
|
+
const normalizedAnyOf = normalizeRootAnyOf(schema);
|
|
214
|
+
|
|
215
|
+
if (schema.$ref === undefined && !normalizedAnyOf) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Some Standard Schema converters emit the root object through a local ref,
|
|
223
|
+
* with the referenced schema stored in a root definition map. Structured
|
|
224
|
+
* Outputs requires the root itself to be an object, so inline that safe,
|
|
225
|
+
* definition-only form while keeping the root maps available for every local
|
|
226
|
+
* pointer in the schema.
|
|
227
|
+
*/
|
|
228
|
+
function inlineRootRefObject(schema: JSONSchema): void {
|
|
229
|
+
let ref = schema.$ref;
|
|
230
|
+
if (ref === undefined) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
assertLocalRootRef(ref);
|
|
235
|
+
if (!hasOnlyRootRefAndDefinitions(schema)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
'Schema $ref at `<root>` has non-metadata siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.',
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const seenRefs = new Set<string>();
|
|
242
|
+
// Ref siblings are annotations in Draft 7, so keep them while following
|
|
243
|
+
// aliases. Add outer annotations first so they win over inner aliases and
|
|
244
|
+
// the final target when the effective root is assembled.
|
|
245
|
+
const inheritedAnnotations: Record<string, unknown> = Object.fromEntries(
|
|
246
|
+
Object.entries(schema).filter(([keyword]) => JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword)),
|
|
247
|
+
);
|
|
248
|
+
let resolved: JSONSchema;
|
|
249
|
+
while (true) {
|
|
250
|
+
if (seenRefs.has(ref)) {
|
|
251
|
+
throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(ref));
|
|
252
|
+
}
|
|
253
|
+
seenRefs.add(ref);
|
|
254
|
+
|
|
255
|
+
const target = resolveLocalRef(schema, ref);
|
|
256
|
+
if (target === undefined) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
'Local $ref at `<root>` does not resolve to an object or boolean schema: ' + JSON.stringify(ref),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (typeof target === 'boolean') {
|
|
262
|
+
throw new TypeError('Expected object schema but got boolean; path=<root>');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const nextRef = target.$ref;
|
|
266
|
+
if (nextRef === undefined) {
|
|
267
|
+
resolved = target;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
assertLocalRootRef(nextRef);
|
|
271
|
+
if (seenRefs.has(nextRef)) {
|
|
272
|
+
throw new Error('Cyclic local $ref at `<root>` is not supported: ' + JSON.stringify(nextRef));
|
|
273
|
+
}
|
|
274
|
+
if (!hasOnlyRefAndAnnotations(target)) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
'Schema $ref in root chain has non-annotation siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.',
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
for (const keyword of JSON_SCHEMA_ANNOTATION_KEYWORDS) {
|
|
280
|
+
if (!(keyword in inheritedAnnotations) && keyword in target) {
|
|
281
|
+
inheritedAnnotations[keyword] = (target as Record<string, unknown>)[keyword];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
ref = nextRef;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const rootDefinitions = schema.$defs;
|
|
288
|
+
const legacyDefinitions = schema.definitions;
|
|
289
|
+
const rootMetadata = Object.fromEntries(
|
|
290
|
+
Object.entries(schema).filter(
|
|
291
|
+
([keyword]) =>
|
|
292
|
+
JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword) || JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword),
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
const inlined = structuredClone(resolved);
|
|
296
|
+
// A target's definition map lives below the target's original pointer, not
|
|
297
|
+
// at the document root. Keep the outer document map at the root so existing
|
|
298
|
+
// absolute refs retain their meaning; the retained outer map also keeps the
|
|
299
|
+
// target's nested map reachable at its original pointer.
|
|
300
|
+
for (const keyword of ['$defs', 'definitions'] as const) {
|
|
301
|
+
if (schema[keyword] !== undefined && inlined[keyword] !== undefined) {
|
|
302
|
+
delete inlined[keyword];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const schemaRecord = schema as Record<string, unknown>;
|
|
306
|
+
|
|
307
|
+
for (const keyword of Object.keys(schema)) {
|
|
308
|
+
delete schemaRecord[keyword];
|
|
309
|
+
}
|
|
310
|
+
Object.assign(schema, inlined, inheritedAnnotations, rootMetadata);
|
|
311
|
+
if (rootDefinitions !== undefined) {
|
|
312
|
+
schema.$defs = rootDefinitions;
|
|
313
|
+
}
|
|
314
|
+
if (legacyDefinitions !== undefined) {
|
|
315
|
+
schema.definitions = legacyDefinitions;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Root object validation runs before recursive strictification, so normalize
|
|
321
|
+
* the same exactly representable allOf forms here that the recursive pass
|
|
322
|
+
* handles for nested schemas.
|
|
323
|
+
*/
|
|
324
|
+
function normalizeRootAllOf(schema: JSONSchema): void {
|
|
325
|
+
while (schema.allOf !== undefined) {
|
|
326
|
+
// Root normalization runs before recursive strictification so the root
|
|
327
|
+
// type check can see an inlined object. Normalize nested intersections in
|
|
328
|
+
// its branches first for the same associative allOf case handled by the
|
|
329
|
+
// later recursive pass.
|
|
330
|
+
if (Array.isArray(schema.allOf)) {
|
|
331
|
+
for (const [index, branch] of schema.allOf.entries()) {
|
|
332
|
+
normalizeObjectAllOfBranches(branch, ['allOf', String(index)], schema);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (mergeObjectAllOf(schema, [], schema)) {
|
|
337
|
+
// Removing neutral true branches can leave a singleton allOf behind;
|
|
338
|
+
// keep normalizing until the root reaches its final object form.
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const allOf = schema.allOf;
|
|
343
|
+
if (!Array.isArray(allOf) || allOf.length !== 1 || !hasOnlyRootAllOfMetadataSiblings(schema)) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const branch = allOf[0];
|
|
348
|
+
if (typeof branch === 'boolean' || !isObject(branch)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const rootMetadata = { ...schema };
|
|
353
|
+
delete rootMetadata.allOf;
|
|
354
|
+
const normalized = structuredClone(branch);
|
|
355
|
+
const schemaRecord = schema as Record<string, unknown>;
|
|
356
|
+
|
|
357
|
+
for (const keyword of Object.keys(schema)) {
|
|
358
|
+
delete schemaRecord[keyword];
|
|
359
|
+
}
|
|
360
|
+
Object.assign(schema, normalized, rootMetadata);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* A singleton root anyOf with no validating siblings is equivalent to its
|
|
366
|
+
* only branch. Flatten it before the root-union check so converters that
|
|
367
|
+
* retain a redundant object wrapper can still produce a strict root object.
|
|
368
|
+
*/
|
|
369
|
+
function normalizeRootAnyOf(schema: JSONSchema): boolean {
|
|
370
|
+
const anyOf = schema.anyOf;
|
|
371
|
+
if (!Array.isArray(anyOf) || !hasOnlyRootAnyOfMetadataSiblings(schema)) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// `false` contributes no instances to a union. Keep the original array in
|
|
376
|
+
// place until promotion so refs into the surviving branch can be rewritten
|
|
377
|
+
// from their original index, while refs into removed false branches become
|
|
378
|
+
// dangling and remain fail closed after the wrapper disappears.
|
|
379
|
+
const realBranches = anyOf
|
|
380
|
+
.map((branch, index) => ({ branch, index }))
|
|
381
|
+
.filter(({ branch }) => branch !== false);
|
|
382
|
+
if (realBranches.length !== 1) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const { branch, index: branchIndex } = realBranches[0]!;
|
|
387
|
+
if (typeof branch === 'boolean' || !isObject(branch) || !isObjectOnlySchema(branch, schema)) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const definitionRenames = planPromotedRootAnyOfDefinitionRenames(schema, branch);
|
|
392
|
+
rewriteLocalRefsIntoPromotedRootAnyOfBranch(schema, branchIndex, definitionRenames);
|
|
393
|
+
const rootMetadata = { ...schema };
|
|
394
|
+
delete rootMetadata.anyOf;
|
|
395
|
+
const normalized = structuredClone(branch);
|
|
396
|
+
|
|
397
|
+
for (const keyword of ['$defs', 'definitions'] as const) {
|
|
398
|
+
const rootDefinitions = schema[keyword];
|
|
399
|
+
const branchDefinitions = normalized[keyword];
|
|
400
|
+
if (!isObject(rootDefinitions) || !isObject(branchDefinitions)) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const renames = definitionRenames.get(keyword);
|
|
405
|
+
const mergedDefinitions: Record<string, JSONSchemaDefinition> = { ...rootDefinitions };
|
|
406
|
+
for (const [name, definition] of Object.entries(branchDefinitions)) {
|
|
407
|
+
mergedDefinitions[renames?.get(name) ?? name] = definition as JSONSchemaDefinition;
|
|
408
|
+
}
|
|
409
|
+
normalized[keyword] = mergedDefinitions;
|
|
410
|
+
delete rootMetadata[keyword];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const schemaRecord = schema as Record<string, unknown>;
|
|
414
|
+
|
|
415
|
+
for (const keyword of Object.keys(schema)) {
|
|
416
|
+
delete schemaRecord[keyword];
|
|
417
|
+
}
|
|
418
|
+
Object.assign(schema, normalized, rootMetadata);
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
type RootDefinitionKeyword = '$defs' | 'definitions';
|
|
423
|
+
type PromotedRootAnyOfDefinitionRenames = Map<RootDefinitionKeyword, Map<string, string>>;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Root and promoted branch definition maps occupy the same pointer after
|
|
427
|
+
* promotion. Give conflicting branch definitions stable aliases before refs
|
|
428
|
+
* are rewritten so neither original target is rebound.
|
|
429
|
+
*/
|
|
430
|
+
function planPromotedRootAnyOfDefinitionRenames(
|
|
431
|
+
root: JSONSchema,
|
|
432
|
+
branch: JSONSchema,
|
|
433
|
+
): PromotedRootAnyOfDefinitionRenames {
|
|
434
|
+
const renames: PromotedRootAnyOfDefinitionRenames = new Map();
|
|
435
|
+
|
|
436
|
+
for (const keyword of ['$defs', 'definitions'] as const) {
|
|
437
|
+
const rootDefinitions = root[keyword];
|
|
438
|
+
const branchDefinitions = branch[keyword];
|
|
439
|
+
if (!isObject(rootDefinitions) || !isObject(branchDefinitions)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const usedNames = new Set([...Object.keys(rootDefinitions), ...Object.keys(branchDefinitions)]);
|
|
444
|
+
const keywordRenames = new Map<string, string>();
|
|
445
|
+
let aliasIndex = 0;
|
|
446
|
+
|
|
447
|
+
for (const [name, definition] of Object.entries(branchDefinitions)) {
|
|
448
|
+
if (
|
|
449
|
+
!Object.prototype.hasOwnProperty.call(rootDefinitions, name) ||
|
|
450
|
+
schemasEqual(rootDefinitions[name], definition)
|
|
451
|
+
) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let alias = '__openai_strict_anyOf_definition_' + aliasIndex++;
|
|
456
|
+
while (usedNames.has(alias)) {
|
|
457
|
+
alias = '__openai_strict_anyOf_definition_' + aliasIndex++;
|
|
458
|
+
}
|
|
459
|
+
usedNames.add(alias);
|
|
460
|
+
keywordRenames.set(name, alias);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (keywordRenames.size > 0) {
|
|
464
|
+
renames.set(keyword, keywordRenames);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return renames;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Promoting a singleton root anyOf branch removes the original anyOf/index
|
|
473
|
+
* pointer prefix. Rewrite refs through that prefix while the old tree still
|
|
474
|
+
* exists so the promoted schema keeps naming the same targets.
|
|
475
|
+
*/
|
|
476
|
+
function rewriteLocalRefsIntoPromotedRootAnyOfBranch(
|
|
477
|
+
root: JSONSchema,
|
|
478
|
+
branchIndex: number,
|
|
479
|
+
definitionRenames: PromotedRootAnyOfDefinitionRenames,
|
|
480
|
+
): void {
|
|
481
|
+
const rewriteRef = (ref: string): string => {
|
|
482
|
+
const parts = parseLocalRef(ref);
|
|
483
|
+
if (parts === undefined || parts[0] !== 'anyOf' || parts[1] !== String(branchIndex)) {
|
|
484
|
+
return ref;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const promotedParts = parts.slice(2);
|
|
488
|
+
const definitionKeyword = promotedParts[0];
|
|
489
|
+
if (promotedParts.length > 1 && (definitionKeyword === '$defs' || definitionKeyword === 'definitions')) {
|
|
490
|
+
const renamed = definitionRenames.get(definitionKeyword)?.get(promotedParts[1]!);
|
|
491
|
+
if (renamed !== undefined) {
|
|
492
|
+
promotedParts[1] = renamed;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return promotedParts.length === 0 ?
|
|
497
|
+
'#'
|
|
498
|
+
: '#/' + promotedParts.map(encodeJSONPointerTokenForURIFragment).join('/');
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const rewriteRefs = (value: JSONSchemaDefinition): void => {
|
|
502
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (typeof value.$ref === 'string') {
|
|
507
|
+
value.$ref = rewriteRef(value.$ref);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
forEachJSONSchemaChild(value, [], (child) => {
|
|
511
|
+
rewriteRefs(child as JSONSchemaDefinition);
|
|
512
|
+
});
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
rewriteRefs(root);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function assertLocalRootRef(ref: unknown): asserts ref is string {
|
|
519
|
+
if (typeof ref !== 'string') {
|
|
520
|
+
throw new TypeError('Received non-string $ref - ' + String(ref) + '; path=<root>');
|
|
521
|
+
}
|
|
522
|
+
if (!ref.startsWith('#')) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
'External $ref at `<root>` is not supported in strict Structured Outputs: ' + JSON.stringify(ref),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function hasOnlyRootRefAndDefinitions(schema: JSONSchema): boolean {
|
|
530
|
+
return Object.keys(schema).every(
|
|
531
|
+
(keyword) =>
|
|
532
|
+
keyword === '$ref' ||
|
|
533
|
+
keyword === '$defs' ||
|
|
534
|
+
keyword === 'definitions' ||
|
|
535
|
+
JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) ||
|
|
536
|
+
JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Draft 7 permits `type` to be either a string or an array of strings. A
|
|
542
|
+
* singleton array has exactly the same validation semantics as its scalar
|
|
543
|
+
* form, so canonicalize it before root validation and recursive strictifying.
|
|
544
|
+
* Multi-type arrays carry real union semantics and must remain unchanged.
|
|
545
|
+
*/
|
|
546
|
+
function normalizeSingletonTypeArrays(schema: JSONSchemaDefinition): void {
|
|
547
|
+
if (typeof schema === 'boolean' || !isObject(schema)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (Array.isArray(schema.type) && schema.type.length === 1) {
|
|
552
|
+
schema.type = schema.type[0]!;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
forEachJSONSchemaChild(schema, [], (child) => {
|
|
556
|
+
normalizeSingletonTypeArrays(child as JSONSchemaDefinition);
|
|
557
|
+
});
|
|
12
558
|
}
|
|
13
559
|
|
|
14
|
-
function isNullable(
|
|
560
|
+
function isNullable(
|
|
561
|
+
schema: JSONSchemaDefinition,
|
|
562
|
+
root: JSONSchema,
|
|
563
|
+
seenRefs: Set<string> = new Set(),
|
|
564
|
+
): boolean {
|
|
15
565
|
if (typeof schema === 'boolean') {
|
|
566
|
+
return schema;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const ref = schema.$ref;
|
|
570
|
+
if (ref !== undefined) {
|
|
571
|
+
// Annotation keywords do not constrain validation, so they are safe beside
|
|
572
|
+
// a local ref. Keep the proof conservative for every other sibling because
|
|
573
|
+
// resolving those correctly would require intersecting the referenced
|
|
574
|
+
// schema and its sibling constraints.
|
|
575
|
+
if (typeof ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(ref)) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const resolved = resolveLocalRef(root, ref);
|
|
580
|
+
if (resolved === undefined) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return isNullable(resolved, root, new Set([...seenRefs, ref]));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (
|
|
588
|
+
schema.type !== undefined &&
|
|
589
|
+
schema.type !== 'null' &&
|
|
590
|
+
!(Array.isArray(schema.type) && schema.type.includes('null'))
|
|
591
|
+
) {
|
|
16
592
|
return false;
|
|
17
593
|
}
|
|
18
|
-
|
|
19
|
-
|
|
594
|
+
|
|
595
|
+
if ('const' in schema && schema.const !== null) {
|
|
596
|
+
return false;
|
|
20
597
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
598
|
+
|
|
599
|
+
if (schema.enum !== undefined && (!Array.isArray(schema.enum) || !schema.enum.includes(null))) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (schema.allOf !== undefined) {
|
|
604
|
+
if (!Array.isArray(schema.allOf) || !schema.allOf.every((variant) => isNullable(variant, root))) {
|
|
605
|
+
return false;
|
|
24
606
|
}
|
|
25
607
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
608
|
+
|
|
609
|
+
if (schema.anyOf !== undefined) {
|
|
610
|
+
if (!Array.isArray(schema.anyOf) || !schema.anyOf.some((variant) => isNullable(variant, root))) {
|
|
611
|
+
return false;
|
|
29
612
|
}
|
|
30
613
|
}
|
|
31
|
-
|
|
614
|
+
|
|
615
|
+
if (schema.oneOf !== undefined) {
|
|
616
|
+
if (
|
|
617
|
+
!Array.isArray(schema.oneOf) ||
|
|
618
|
+
schema.oneOf.filter((variant) => isNullable(variant, root)).length !== 1
|
|
619
|
+
) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Conditional and negated schemas need a full JSON Schema evaluator to prove
|
|
625
|
+
// that null is allowed. Treat them conservatively instead of accepting an
|
|
626
|
+
// optional field that may not actually accept null.
|
|
627
|
+
if (
|
|
628
|
+
schema.not !== undefined ||
|
|
629
|
+
schema.if !== undefined ||
|
|
630
|
+
schema.then !== undefined ||
|
|
631
|
+
schema.else !== undefined
|
|
632
|
+
) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return true;
|
|
32
637
|
}
|
|
33
638
|
|
|
34
639
|
/**
|
|
@@ -48,146 +653,1368 @@ function ensureStrictJsonSchema(
|
|
|
48
653
|
throw new TypeError(`Expected ${JSON.stringify(jsonSchema)} to be an object; path=${path.join('/')}`);
|
|
49
654
|
}
|
|
50
655
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
656
|
+
// Closing each object branch in an allOf independently changes the
|
|
657
|
+
// intersection: sibling branches' properties become forbidden extras. Merge
|
|
658
|
+
// the small object-intersection subset that can be represented exactly before
|
|
659
|
+
// applying strict object closure, and fail closed for the rest.
|
|
660
|
+
if (mergeObjectAllOf(jsonSchema, path, root)) {
|
|
661
|
+
return ensureStrictJsonSchema(jsonSchema, path, root);
|
|
57
662
|
}
|
|
58
663
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
664
|
+
// false is the identity element for a union. Remove it before proving
|
|
665
|
+
// whether an outer object/array wrapper is redundant so an impossible
|
|
666
|
+
// alternative cannot hide the shape of every real branch. Keep all-false
|
|
667
|
+
// and boolean-only unions intact so the existing boolean-schema rejection
|
|
668
|
+
// remains fail closed.
|
|
669
|
+
normalizeAnyOfFalseBranches(jsonSchema);
|
|
66
670
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
671
|
+
// Closing a type: object wrapper around object union branches without any
|
|
672
|
+
// own properties would turn it into an empty object and forbid every branch
|
|
673
|
+
// property. A bare object type is redundant when every branch already proves
|
|
674
|
+
// the value is an object, so remove only that exact constraint; fail closed
|
|
675
|
+
// for wrappers whose object constraints cannot be preserved mechanically.
|
|
676
|
+
normalizeObjectUnionWrapper(jsonSchema, path, root);
|
|
677
|
+
|
|
678
|
+
// Add additionalProperties: false to object schemas. Draft 7 permits object
|
|
679
|
+
// keywords without an explicit type, so those implicit object shapes need
|
|
680
|
+
// the same strict handling as type: 'object'. Explicitly open object schemas
|
|
681
|
+
// cannot be represented in Structured Outputs strict mode.
|
|
682
|
+
if (hasObjectShape(jsonSchema)) {
|
|
683
|
+
if (!('additionalProperties' in jsonSchema)) {
|
|
684
|
+
jsonSchema.additionalProperties = false;
|
|
685
|
+
} else if (jsonSchema.additionalProperties !== false) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`Object schema at \`${
|
|
688
|
+
path.join('/') || '<root>'
|
|
689
|
+
}\` must set \`additionalProperties: false\` to be compatible with strict Structured Outputs.`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
71
692
|
}
|
|
72
693
|
|
|
73
694
|
const required = jsonSchema.required ?? [];
|
|
695
|
+
if (!Array.isArray(required) || required.some((key) => typeof key !== 'string')) {
|
|
696
|
+
throw new TypeError(
|
|
697
|
+
`Expected \`required\` to be an array of strings; path=${path.join('/') || '<root>'}`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
74
700
|
|
|
75
701
|
// Handle object properties
|
|
76
702
|
const properties = jsonSchema.properties;
|
|
703
|
+
if (hasObjectShape(jsonSchema)) {
|
|
704
|
+
for (const key of required) {
|
|
705
|
+
if (!isObject(properties) || !Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`Object schema at \`${
|
|
708
|
+
path.join('/') || '<root>'
|
|
709
|
+
}\` requires property \`${key}\` but does not declare it in \`properties\`.`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
77
714
|
if (isObject(properties)) {
|
|
78
715
|
for (const [key, value] of Object.entries(properties)) {
|
|
79
|
-
if (!isNullable(value) && !required.includes(key)) {
|
|
716
|
+
if (!isNullable(value, root) && !required.includes(key)) {
|
|
80
717
|
throw new Error(
|
|
81
|
-
`
|
|
718
|
+
`Schema field at \`${[...path, 'properties', key].join(
|
|
82
719
|
'/',
|
|
83
720
|
)}\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required`,
|
|
84
721
|
);
|
|
85
722
|
}
|
|
86
723
|
}
|
|
87
724
|
jsonSchema.required = Object.keys(properties);
|
|
88
|
-
jsonSchema.properties = Object.fromEntries(
|
|
89
|
-
Object.entries(properties).map(([key, propSchema]) => [
|
|
90
|
-
key,
|
|
91
|
-
ensureStrictJsonSchema(propSchema, [...path, 'properties', key], root),
|
|
92
|
-
]),
|
|
93
|
-
);
|
|
94
725
|
}
|
|
95
726
|
|
|
96
|
-
//
|
|
727
|
+
// Structured Outputs accepts one schema for every array item and does not
|
|
728
|
+
// support Draft 7 tuples or additionalItems. Reject both forms rather than
|
|
729
|
+
// advertising a schema whose array validation the API cannot preserve.
|
|
97
730
|
const items = jsonSchema.items;
|
|
98
|
-
|
|
99
|
-
|
|
731
|
+
const additionalItems = jsonSchema.additionalItems;
|
|
732
|
+
if (Array.isArray(items)) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Schema at \`${
|
|
735
|
+
path.join('/') || '<root>'
|
|
736
|
+
}\` uses tuple-form \`items\`, which cannot be represented in strict Structured Outputs.`,
|
|
737
|
+
);
|
|
100
738
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
ensureStrictJsonSchema(variant, [...path, 'anyOf', String(i)], root),
|
|
739
|
+
if (additionalItems !== undefined) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
`Schema at \`${
|
|
742
|
+
path.join('/') || '<root>'
|
|
743
|
+
}\` uses unsupported keyword \`additionalItems\` and cannot be represented in strict Structured Outputs.`,
|
|
107
744
|
);
|
|
108
745
|
}
|
|
109
746
|
|
|
110
747
|
// Handle intersections (allOf)
|
|
111
748
|
const allOf = jsonSchema.allOf;
|
|
112
749
|
if (Array.isArray(allOf)) {
|
|
113
|
-
if (allOf.length === 1) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
750
|
+
if (allOf.length === 1 && hasOnlyAnnotationSiblings(jsonSchema, 'allOf')) {
|
|
751
|
+
const branch = allOf[0]!;
|
|
752
|
+
if (branch === false) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
`Schema at \`${
|
|
755
|
+
path.join('/') || '<root>'
|
|
756
|
+
}\` uses \`allOf: [false]\`, which cannot be represented in strict Structured Outputs.`,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (branch === true) {
|
|
760
|
+
// true is the neutral schema for an intersection, so removing this
|
|
761
|
+
// branch preserves validation while retaining the parent annotations.
|
|
762
|
+
delete jsonSchema.allOf;
|
|
763
|
+
} else {
|
|
764
|
+
const resolved = ensureStrictJsonSchema(branch, [...path, 'allOf', '0'], root);
|
|
765
|
+
const annotations = { ...jsonSchema };
|
|
766
|
+
delete annotations.allOf;
|
|
767
|
+
Object.assign(jsonSchema, resolved, annotations);
|
|
768
|
+
delete jsonSchema.allOf;
|
|
769
|
+
}
|
|
121
770
|
}
|
|
122
771
|
}
|
|
123
772
|
|
|
124
|
-
|
|
125
|
-
if (jsonSchema.default === null) {
|
|
126
|
-
delete jsonSchema.default;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Handle $ref with additional properties
|
|
130
|
-
const ref = (jsonSchema as any).$ref;
|
|
131
|
-
if (ref && hasMoreThanNKeys(jsonSchema, 1)) {
|
|
132
|
-
if (typeof ref !== 'string') {
|
|
133
|
-
throw new TypeError(`Received non-string $ref - ${ref}; path=${path.join('/')}`);
|
|
134
|
-
}
|
|
773
|
+
normalizeArrayUnionWrapper(jsonSchema, root);
|
|
135
774
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
775
|
+
const schemaRecord = jsonSchema as Record<string, unknown>;
|
|
776
|
+
for (const keyword of JSON_SCHEMA_UNSUPPORTED_SCHEMA_KEYWORDS) {
|
|
777
|
+
// Optional converter output often keeps undefined placeholders on the
|
|
778
|
+
// JavaScript object even though JSON serialization omits them. They carry
|
|
779
|
+
// no validation semantics, so treat only undefined as absent; every
|
|
780
|
+
// defined value remains unsupported.
|
|
781
|
+
if (schemaRecord[keyword] !== undefined) {
|
|
141
782
|
throw new Error(
|
|
142
|
-
`
|
|
783
|
+
`Schema at \`${
|
|
784
|
+
path.join('/') || '<root>'
|
|
785
|
+
}\` uses unsupported keyword \`${keyword}\` and cannot be represented in strict Structured Outputs.`,
|
|
143
786
|
);
|
|
144
787
|
}
|
|
788
|
+
delete schemaRecord[keyword];
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const type = jsonSchema.type;
|
|
792
|
+
const currentItems = jsonSchema.items;
|
|
793
|
+
if ((type === 'array' || (Array.isArray(type) && type.includes('array'))) && currentItems === undefined) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`Schema at \`${
|
|
796
|
+
path.join('/') || '<root>'
|
|
797
|
+
}\` declares an array without \`items\`, which cannot be represented in strict Structured Outputs.`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
forEachJSONSchemaChild(jsonSchema, path, (child, childPath, keyword) => {
|
|
802
|
+
// These boolean forms are already handled as parent-keyword semantics:
|
|
803
|
+
// additionalProperties: false closes objects, while boolean
|
|
804
|
+
// additionalItems does not contain a nested schema to strictify.
|
|
805
|
+
if (typeof child === 'boolean' && (keyword === 'additionalProperties' || keyword === 'additionalItems')) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
145
808
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
delete (jsonSchema as any).$ref;
|
|
809
|
+
ensureStrictJsonSchema(child as JSONSchemaDefinition, childPath, root);
|
|
810
|
+
});
|
|
149
811
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
812
|
+
// Strip `null` defaults as there's no meaningful distinction
|
|
813
|
+
if (jsonSchema.default === null) {
|
|
814
|
+
delete jsonSchema.default;
|
|
153
815
|
}
|
|
154
816
|
|
|
155
817
|
return jsonSchema;
|
|
156
818
|
}
|
|
157
819
|
|
|
158
|
-
function
|
|
159
|
-
if (!ref.startsWith('
|
|
160
|
-
|
|
820
|
+
function parseLocalRef(ref: string): string[] | undefined {
|
|
821
|
+
if (!ref.startsWith('#')) {
|
|
822
|
+
return undefined;
|
|
161
823
|
}
|
|
162
824
|
|
|
163
|
-
|
|
164
|
-
|
|
825
|
+
let pointer: string;
|
|
826
|
+
try {
|
|
827
|
+
// A local $ref is a URI fragment containing a JSON Pointer. RFC 6901
|
|
828
|
+
// decodes the complete fragment before tokenizing the pointer, so an
|
|
829
|
+
// encoded slash is a separator rather than part of a literal key.
|
|
830
|
+
pointer = decodeURIComponent(ref.slice(1));
|
|
831
|
+
} catch {
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
165
834
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
835
|
+
if (pointer === '') {
|
|
836
|
+
return [];
|
|
837
|
+
}
|
|
838
|
+
if (!pointer.startsWith('/')) {
|
|
839
|
+
return undefined;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const parts: string[] = [];
|
|
843
|
+
for (const encodedPart of pointer.slice(1).split('/')) {
|
|
844
|
+
// JSON Pointer only defines ~0 and ~1 escapes. Reject malformed escape
|
|
845
|
+
// sequences instead of looking up a different literal key.
|
|
846
|
+
if (/~(?:[^01]|$)/.test(encodedPart)) {
|
|
847
|
+
return undefined;
|
|
173
848
|
}
|
|
174
|
-
|
|
849
|
+
parts.push(encodedPart.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
175
850
|
}
|
|
176
851
|
|
|
177
|
-
return
|
|
852
|
+
return parts;
|
|
178
853
|
}
|
|
179
854
|
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
855
|
+
function resolvePointerPart(resolved: unknown, part: string): unknown | undefined {
|
|
856
|
+
if (Array.isArray(resolved)) {
|
|
857
|
+
if (!/^(?:0|[1-9]\d*)$/.test(part)) {
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
183
860
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
i++;
|
|
188
|
-
if (i > n) {
|
|
189
|
-
return true;
|
|
861
|
+
const index = Number(part);
|
|
862
|
+
if (!Object.prototype.hasOwnProperty.call(resolved, index)) {
|
|
863
|
+
return undefined;
|
|
190
864
|
}
|
|
865
|
+
return resolved[index];
|
|
191
866
|
}
|
|
192
|
-
|
|
867
|
+
|
|
868
|
+
if (!isObject(resolved) || !Object.prototype.hasOwnProperty.call(resolved, part)) {
|
|
869
|
+
return undefined;
|
|
870
|
+
}
|
|
871
|
+
return resolved[part];
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function resolveLocalRef(root: JSONSchema, ref: string): JSONSchemaDefinition | undefined {
|
|
875
|
+
const parts = parseLocalRef(ref);
|
|
876
|
+
if (parts === undefined) {
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Literal payloads such as `default`, `enum`, and `const` can contain
|
|
881
|
+
// object-shaped values, but they are not schemas and are never traversed by
|
|
882
|
+
// strictification. Resolve only the schema-bearing locations visited by
|
|
883
|
+
// forEachJSONSchemaChild so every accepted target is normalized before we
|
|
884
|
+
// advertise the result as strict.
|
|
885
|
+
let resolved: unknown = root;
|
|
886
|
+
for (let index = 0; index < parts.length; ) {
|
|
887
|
+
if (!isObject(resolved)) {
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const keyword = parts[index]!;
|
|
892
|
+
if (JSON_SCHEMA_SINGLE_SCHEMA_KEYWORDS.includes(keyword)) {
|
|
893
|
+
resolved = resolvePointerPart(resolved, keyword);
|
|
894
|
+
index += 1;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if (JSON_SCHEMA_ARRAY_SCHEMA_KEYWORDS.includes(keyword)) {
|
|
899
|
+
resolved = resolvePointerPart(resolved, keyword);
|
|
900
|
+
index += 1;
|
|
901
|
+
if (Array.isArray(resolved)) {
|
|
902
|
+
if (index >= parts.length) {
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
resolved = resolvePointerPart(resolved, parts[index]!);
|
|
906
|
+
index += 1;
|
|
907
|
+
}
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (JSON_SCHEMA_MAP_SCHEMA_KEYWORDS.includes(keyword)) {
|
|
912
|
+
const children = resolvePointerPart(resolved, keyword);
|
|
913
|
+
index += 1;
|
|
914
|
+
if (!isObject(children) || index >= parts.length) {
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
resolved = resolvePointerPart(children, parts[index]!);
|
|
919
|
+
if (keyword === 'dependencies' && !isSchemaDefinition(resolved)) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
index += 1;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return isSchemaDefinition(resolved) ? resolved : undefined;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function isObject<T>(obj: T | Array<any>): obj is Extract<T, Record<string, any>> {
|
|
933
|
+
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function isSchemaDefinition(value: unknown): value is JSONSchemaDefinition {
|
|
937
|
+
return typeof value === 'boolean' || isObject(value);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function isObjectOnlySchema(
|
|
941
|
+
schema: JSONSchemaDefinition,
|
|
942
|
+
root: JSONSchema,
|
|
943
|
+
seenRefs: Set<string> = new Set(),
|
|
944
|
+
): boolean {
|
|
945
|
+
if (typeof schema === 'boolean' || !isObject(schema)) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (schema.$ref !== undefined) {
|
|
950
|
+
if (typeof schema.$ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(schema.$ref)) {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const resolved = resolveLocalRef(root, schema.$ref);
|
|
955
|
+
if (resolved === undefined) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return isObjectOnlySchema(resolved, root, new Set([...seenRefs, schema.$ref]));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (schema.allOf !== undefined) {
|
|
963
|
+
if (
|
|
964
|
+
!Array.isArray(schema.allOf) ||
|
|
965
|
+
schema.allOf.length !== 1 ||
|
|
966
|
+
!hasOnlyAnnotationSiblings(schema, 'allOf')
|
|
967
|
+
) {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const branch = schema.allOf[0];
|
|
972
|
+
return branch !== undefined && branch !== true && branch !== false ?
|
|
973
|
+
isObjectOnlySchema(branch, root, seenRefs)
|
|
974
|
+
: false;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return (
|
|
978
|
+
schema.type === 'object' ||
|
|
979
|
+
(Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === 'object')
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function isArrayOnlySchema(
|
|
984
|
+
schema: JSONSchemaDefinition,
|
|
985
|
+
root: JSONSchema,
|
|
986
|
+
seenRefs: Set<string> = new Set(),
|
|
987
|
+
): boolean {
|
|
988
|
+
if (typeof schema === 'boolean' || !isObject(schema)) {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (schema.$ref !== undefined) {
|
|
993
|
+
if (typeof schema.$ref !== 'string' || !hasOnlyRefAndAnnotations(schema) || seenRefs.has(schema.$ref)) {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const resolved = resolveLocalRef(root, schema.$ref);
|
|
998
|
+
if (resolved === undefined) {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return isArrayOnlySchema(resolved, root, new Set([...seenRefs, schema.$ref]));
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (schema.allOf !== undefined) {
|
|
1006
|
+
if (
|
|
1007
|
+
!Array.isArray(schema.allOf) ||
|
|
1008
|
+
schema.allOf.length !== 1 ||
|
|
1009
|
+
!hasOnlyAnnotationSiblings(schema, 'allOf')
|
|
1010
|
+
) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const branch = schema.allOf[0];
|
|
1015
|
+
return branch !== undefined && branch !== true && branch !== false ?
|
|
1016
|
+
isArrayOnlySchema(branch, root, seenRefs)
|
|
1017
|
+
: false;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return (
|
|
1021
|
+
schema.type === 'array' ||
|
|
1022
|
+
(Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === 'array')
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export function hasOnlyRefAndAnnotations(schema: JSONSchema): boolean {
|
|
1027
|
+
return Object.keys(schema).every(
|
|
1028
|
+
// Definition maps do not add sibling validation constraints, and keeping
|
|
1029
|
+
// them in place preserves local pointers into a nested alias's scope.
|
|
1030
|
+
(keyword) =>
|
|
1031
|
+
keyword === '$ref' ||
|
|
1032
|
+
keyword === '$defs' ||
|
|
1033
|
+
keyword === 'definitions' ||
|
|
1034
|
+
JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function hasOnlyAnnotationSiblings(schema: JSONSchema, keyword: string): boolean {
|
|
1039
|
+
const schemaRecord = schema as Record<string, unknown>;
|
|
1040
|
+
return Object.keys(schema).every(
|
|
1041
|
+
// Definition maps do not add sibling validation constraints. Keep them
|
|
1042
|
+
// beside a flattened singleton allOf so refs into this nested scope stay
|
|
1043
|
+
// reachable at their original pointers.
|
|
1044
|
+
(schemaKeyword) =>
|
|
1045
|
+
schemaKeyword === keyword ||
|
|
1046
|
+
((schemaKeyword === '$defs' || schemaKeyword === 'definitions') &&
|
|
1047
|
+
isObject(schemaRecord[schemaKeyword])) ||
|
|
1048
|
+
JSON_SCHEMA_ANNOTATION_KEYWORDS.has(schemaKeyword),
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function hasOnlyRootAllOfMetadataSiblings(schema: JSONSchema): boolean {
|
|
1053
|
+
return Object.keys(schema).every(
|
|
1054
|
+
(keyword) =>
|
|
1055
|
+
keyword === 'allOf' ||
|
|
1056
|
+
keyword === '$defs' ||
|
|
1057
|
+
keyword === 'definitions' ||
|
|
1058
|
+
JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) ||
|
|
1059
|
+
JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function hasOnlyRootAnyOfMetadataSiblings(schema: JSONSchema): boolean {
|
|
1064
|
+
return Object.keys(schema).every(
|
|
1065
|
+
(keyword) =>
|
|
1066
|
+
keyword === 'anyOf' ||
|
|
1067
|
+
keyword === '$defs' ||
|
|
1068
|
+
keyword === 'definitions' ||
|
|
1069
|
+
(keyword === 'type' && schema.type === 'object') ||
|
|
1070
|
+
JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword) ||
|
|
1071
|
+
JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword),
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function hasObjectKeywords(schema: JSONSchema): boolean {
|
|
1076
|
+
return Object.keys(schema).some((keyword) => JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function hasObjectShape(schema: JSONSchema): boolean {
|
|
1080
|
+
const typ = schema.type;
|
|
1081
|
+
return (
|
|
1082
|
+
typ === 'object' ||
|
|
1083
|
+
(Array.isArray(typ) && typ.includes('object')) ||
|
|
1084
|
+
(typ === undefined && hasObjectKeywords(schema))
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function isRedundantUnionWrapperType(type: JSONSchema['type'], branchType: 'object' | 'array'): boolean {
|
|
1089
|
+
return (
|
|
1090
|
+
type === branchType ||
|
|
1091
|
+
(Array.isArray(type) && type.length === 2 && type.includes(branchType) && type.includes('null'))
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function normalizeObjectUnionWrapper(jsonSchema: JSONSchema, path: string[], root: JSONSchema): void {
|
|
1096
|
+
if (jsonSchema.anyOf === undefined) {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const hasEmptyProperties =
|
|
1101
|
+
isObject(jsonSchema.properties) && Object.keys(jsonSchema.properties).length === 0;
|
|
1102
|
+
const hasEmptyRequired = Array.isArray(jsonSchema.required) && jsonSchema.required.length === 0;
|
|
1103
|
+
if (hasEmptyProperties) {
|
|
1104
|
+
delete jsonSchema.properties;
|
|
1105
|
+
}
|
|
1106
|
+
if (hasEmptyRequired) {
|
|
1107
|
+
delete jsonSchema.required;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (!hasObjectShape(jsonSchema)) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const hasOwnObjectConstraints = Object.keys(jsonSchema).some((keyword) =>
|
|
1115
|
+
JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword),
|
|
1116
|
+
);
|
|
1117
|
+
if (
|
|
1118
|
+
isRedundantUnionWrapperType(jsonSchema.type, 'object') &&
|
|
1119
|
+
!hasOwnObjectConstraints &&
|
|
1120
|
+
Array.isArray(jsonSchema.anyOf) &&
|
|
1121
|
+
jsonSchema.anyOf.every((branch) => isObjectOnlySchema(branch, root))
|
|
1122
|
+
) {
|
|
1123
|
+
// The union already excludes null and every non-object value, so both a
|
|
1124
|
+
// scalar object wrapper and a nullable object wrapper are redundant under
|
|
1125
|
+
// Draft 7's conjunctive keyword semantics.
|
|
1126
|
+
delete jsonSchema.type;
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
'Object anyOf schema at `' +
|
|
1132
|
+
(path.join('/') || '<root>') +
|
|
1133
|
+
'` cannot be represented in strict Structured Outputs without changing Draft 7 validation.',
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function normalizeArrayUnionWrapper(jsonSchema: JSONSchema, root: JSONSchema): void {
|
|
1138
|
+
if (
|
|
1139
|
+
isRedundantUnionWrapperType(jsonSchema.type, 'array') &&
|
|
1140
|
+
jsonSchema.items === undefined &&
|
|
1141
|
+
Array.isArray(jsonSchema.anyOf) &&
|
|
1142
|
+
jsonSchema.anyOf.every((branch) => isArrayOnlySchema(branch, root))
|
|
1143
|
+
) {
|
|
1144
|
+
// Every union branch already proves the value is an array and excludes
|
|
1145
|
+
// null. Keeping either redundant wrapper would require an outer items
|
|
1146
|
+
// schema that does not contribute any Draft 7 validation.
|
|
1147
|
+
delete jsonSchema.type;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function normalizeAnyOfFalseBranches(jsonSchema: JSONSchema): void {
|
|
1152
|
+
if (!Array.isArray(jsonSchema.anyOf)) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const realBranches = jsonSchema.anyOf.filter((branch) => branch !== false);
|
|
1157
|
+
if (realBranches.length > 0 && realBranches.length !== jsonSchema.anyOf.length) {
|
|
1158
|
+
jsonSchema.anyOf = realBranches;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
export function assertNoNestedSchemaIds(schema: JSONSchema): void {
|
|
1163
|
+
const visit = (value: JSONSchemaDefinition, path: string[]): void => {
|
|
1164
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (path.length > 0 && value.$id !== undefined) {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
'Nested $id at ' +
|
|
1171
|
+
JSON.stringify(path.join('/')) +
|
|
1172
|
+
' establishes a separate JSON Schema resource scope and cannot be represented in strict Structured Outputs.',
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
forEachJSONSchemaChild(value, path, (child, childPath) => {
|
|
1177
|
+
visit(child as JSONSchemaDefinition, childPath);
|
|
1178
|
+
});
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
visit(schema, []);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function refTargetsAllOfBranch(root: JSONSchema, ref: string): boolean {
|
|
1185
|
+
const parts = parseLocalRef(ref);
|
|
1186
|
+
if (parts === undefined) {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
let resolved: unknown = root;
|
|
1191
|
+
for (const [index, part] of parts.entries()) {
|
|
1192
|
+
if (
|
|
1193
|
+
part === 'allOf' &&
|
|
1194
|
+
isObject(resolved) &&
|
|
1195
|
+
Array.isArray(resolved['allOf']) &&
|
|
1196
|
+
index < parts.length - 1
|
|
1197
|
+
) {
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
resolved = resolvePointerPart(resolved, part);
|
|
1202
|
+
if (resolved === undefined) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function escapeJSONPointerToken(token: string): string {
|
|
1211
|
+
return token.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function encodeJSONPointerTokenForURIFragment(token: string): string {
|
|
1215
|
+
// `$` is a valid URI fragment sub-delimiter and keeping it readable retains
|
|
1216
|
+
// the conventional `#/$defs/...` spelling. Everything else that could
|
|
1217
|
+
// invalidate or retokenize the fragment (notably `%` and spaces) is encoded
|
|
1218
|
+
// after JSON Pointer escaping.
|
|
1219
|
+
return encodeURIComponent(escapeJSONPointerToken(token)).replace(/%24/g, '$');
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Standard Schema normalization moves representable oneOf branches to anyOf.
|
|
1224
|
+
* Rewrite only pointers that traverse an actual oneOf schema array while the
|
|
1225
|
+
* original tree is still intact, preserving escaped tokens for every other
|
|
1226
|
+
* path segment.
|
|
1227
|
+
*/
|
|
1228
|
+
export function rewriteLocalRefsIntoMovedOneOfBranches(root: JSONSchema): void {
|
|
1229
|
+
const rewriteRef = (ref: string): string => {
|
|
1230
|
+
const parts = parseLocalRef(ref);
|
|
1231
|
+
if (parts === undefined || parts.length === 0) {
|
|
1232
|
+
return ref;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
let resolved: unknown = root;
|
|
1236
|
+
let changed = false;
|
|
1237
|
+
for (const [index, part] of parts.entries()) {
|
|
1238
|
+
if (
|
|
1239
|
+
part === 'oneOf' &&
|
|
1240
|
+
index < parts.length - 1 &&
|
|
1241
|
+
isObject(resolved) &&
|
|
1242
|
+
Array.isArray(resolved['oneOf'])
|
|
1243
|
+
) {
|
|
1244
|
+
parts[index] = 'anyOf';
|
|
1245
|
+
changed = true;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
resolved = resolvePointerPart(resolved, part);
|
|
1249
|
+
if (resolved === undefined) {
|
|
1250
|
+
return ref;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return changed ? '#/' + parts.map(encodeJSONPointerTokenForURIFragment).join('/') : ref;
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const rewriteRefs = (value: JSONSchemaDefinition): void => {
|
|
1258
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (typeof value.$ref === 'string') {
|
|
1263
|
+
value.$ref = rewriteRef(value.$ref);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
forEachJSONSchemaChild(value, [], (child) => {
|
|
1267
|
+
rewriteRefs(child as JSONSchemaDefinition);
|
|
1268
|
+
});
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
rewriteRefs(root);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Strictification removes false anyOf alternatives. Rewrite pointers into
|
|
1276
|
+
* surviving alternatives before that filtering happens so each local ref
|
|
1277
|
+
* still names the same schema after earlier indices disappear.
|
|
1278
|
+
*/
|
|
1279
|
+
function rewriteLocalRefsIntoFilteredAnyOfBranches(root: JSONSchema): void {
|
|
1280
|
+
const rewriteRef = (ref: string): string => {
|
|
1281
|
+
const originalParts = parseLocalRef(ref);
|
|
1282
|
+
if (originalParts === undefined || originalParts.length === 0) {
|
|
1283
|
+
return ref;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const rewrittenParts = [...originalParts];
|
|
1287
|
+
let resolved: unknown = root;
|
|
1288
|
+
let changed = false;
|
|
1289
|
+
|
|
1290
|
+
for (const [index, part] of originalParts.entries()) {
|
|
1291
|
+
const resolvedRecord =
|
|
1292
|
+
typeof resolved === 'object' && resolved !== null && !Array.isArray(resolved) ?
|
|
1293
|
+
(resolved as Record<string, unknown>)
|
|
1294
|
+
: undefined;
|
|
1295
|
+
if (
|
|
1296
|
+
part === 'anyOf' &&
|
|
1297
|
+
index < originalParts.length - 1 &&
|
|
1298
|
+
resolvedRecord !== undefined &&
|
|
1299
|
+
Array.isArray(resolvedRecord['anyOf'])
|
|
1300
|
+
) {
|
|
1301
|
+
const branches = resolvedRecord['anyOf'];
|
|
1302
|
+
const branchIndexPart = originalParts[index + 1]!;
|
|
1303
|
+
if (!/^(?:0|[1-9]\d*)$/.test(branchIndexPart)) {
|
|
1304
|
+
return ref;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const branchIndex = Number(branchIndexPart);
|
|
1308
|
+
if (!Object.prototype.hasOwnProperty.call(branches, branchIndex)) {
|
|
1309
|
+
return ref;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const realBranches = branches.filter((branch) => branch !== false);
|
|
1313
|
+
if (realBranches.length > 0 && realBranches.length !== branches.length) {
|
|
1314
|
+
// A ref to a removed false schema is already rejected by the first
|
|
1315
|
+
// ref-validation pass. Leave it untouched so it remains fail closed.
|
|
1316
|
+
if (branches[branchIndex] === false) {
|
|
1317
|
+
return ref;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const rewrittenIndex = branches.slice(0, branchIndex).filter((branch) => branch !== false).length;
|
|
1321
|
+
if (rewrittenIndex !== branchIndex) {
|
|
1322
|
+
rewrittenParts[index + 1] = String(rewrittenIndex);
|
|
1323
|
+
changed = true;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Resolve through the original pointer, not the rewritten one, so a
|
|
1329
|
+
// nested anyOf can also be remapped after its parent index shifts.
|
|
1330
|
+
resolved = resolvePointerPart(resolved, part);
|
|
1331
|
+
if (resolved === undefined) {
|
|
1332
|
+
return ref;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return changed ? '#/' + rewrittenParts.map(encodeJSONPointerTokenForURIFragment).join('/') : ref;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
const rewriteRefs = (value: JSONSchemaDefinition): void => {
|
|
1340
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (typeof value.$ref === 'string') {
|
|
1345
|
+
value.$ref = rewriteRef(value.$ref);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
forEachJSONSchemaChild(value, [], (child) => {
|
|
1349
|
+
rewriteRefs(child as JSONSchemaDefinition);
|
|
1350
|
+
});
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
rewriteRefs(root);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Strictification removes every representable allOf. Preserve any schema
|
|
1358
|
+
* referenced through an allOf branch under a stable root definition first so
|
|
1359
|
+
* structural flattening cannot leave a dangling local pointer behind.
|
|
1360
|
+
*/
|
|
1361
|
+
function preserveAllOfRefTargets(root: JSONSchema, rootOnly = false): void {
|
|
1362
|
+
const refsToPreserve = new Set<string>();
|
|
1363
|
+
const collectRefs = (value: JSONSchemaDefinition): void => {
|
|
1364
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (typeof value.$ref === 'string' && refTargetsAllOfBranch(root, value.$ref)) {
|
|
1369
|
+
const pointerParts = parseLocalRef(value.$ref);
|
|
1370
|
+
if (!rootOnly || pointerParts?.[0] === 'allOf') {
|
|
1371
|
+
refsToPreserve.add(value.$ref);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
forEachJSONSchemaChild(value, [], (child) => {
|
|
1376
|
+
collectRefs(child as JSONSchemaDefinition);
|
|
1377
|
+
});
|
|
1378
|
+
};
|
|
1379
|
+
collectRefs(root);
|
|
1380
|
+
|
|
1381
|
+
if (refsToPreserve.size === 0) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (root.$defs !== undefined && !isObject(root.$defs)) {
|
|
1386
|
+
throw new Error('Root schema has invalid $defs and cannot preserve local allOf references.');
|
|
1387
|
+
}
|
|
1388
|
+
const definitions = (root.$defs ??= {});
|
|
1389
|
+
const rewrittenRefs = new Map<string, string>();
|
|
1390
|
+
let aliasIndex = 0;
|
|
1391
|
+
|
|
1392
|
+
for (const ref of refsToPreserve) {
|
|
1393
|
+
const target = resolveLocalRef(root, ref);
|
|
1394
|
+
if (!isSchemaDefinition(target)) {
|
|
1395
|
+
if (rootOnly) {
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
throw new Error('Local $ref cannot be preserved before allOf flattening: ' + JSON.stringify(ref));
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
let alias = '__openai_strict_allOf_ref_' + aliasIndex++;
|
|
1402
|
+
while (Object.prototype.hasOwnProperty.call(definitions, alias)) {
|
|
1403
|
+
alias = '__openai_strict_allOf_ref_' + aliasIndex++;
|
|
1404
|
+
}
|
|
1405
|
+
definitions[alias] = structuredClone(target);
|
|
1406
|
+
rewrittenRefs.set(ref, '#/$defs/' + escapeJSONPointerToken(alias));
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const rewriteRefs = (value: JSONSchemaDefinition): void => {
|
|
1410
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
if (typeof value.$ref === 'string') {
|
|
1415
|
+
value.$ref = rewrittenRefs.get(value.$ref) ?? value.$ref;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
forEachJSONSchemaChild(value, [], (child) => {
|
|
1419
|
+
rewriteRefs(child as JSONSchemaDefinition);
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1422
|
+
rewriteRefs(root);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Closed allOf merges can discard optional property declarations. Preserve
|
|
1427
|
+
* only local refs into declarations that are about to disappear, then rewrite
|
|
1428
|
+
* those refs to stable root definitions before the merge removes their paths.
|
|
1429
|
+
*/
|
|
1430
|
+
function preserveDiscardedAllOfPropertyRefTargets(root: JSONSchema, discardedPaths: string[][]): void {
|
|
1431
|
+
if (discardedPaths.length === 0) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const refsToPreserve = new Set<string>();
|
|
1436
|
+
const collectRefs = (value: JSONSchemaDefinition): void => {
|
|
1437
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (typeof value.$ref === 'string') {
|
|
1442
|
+
const parts = parseLocalRef(value.$ref);
|
|
1443
|
+
if (
|
|
1444
|
+
parts !== undefined &&
|
|
1445
|
+
discardedPaths.some(
|
|
1446
|
+
(discardedPath) =>
|
|
1447
|
+
parts.length >= discardedPath.length &&
|
|
1448
|
+
discardedPath.every((part, index) => parts[index] === part),
|
|
1449
|
+
)
|
|
1450
|
+
) {
|
|
1451
|
+
refsToPreserve.add(value.$ref);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
forEachJSONSchemaChild(value, [], (child) => {
|
|
1456
|
+
collectRefs(child as JSONSchemaDefinition);
|
|
1457
|
+
});
|
|
1458
|
+
};
|
|
1459
|
+
collectRefs(root);
|
|
1460
|
+
|
|
1461
|
+
if (refsToPreserve.size === 0) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (root.$defs !== undefined && !isObject(root.$defs)) {
|
|
1466
|
+
throw new Error('Root schema has invalid $defs and cannot preserve discarded allOf properties.');
|
|
1467
|
+
}
|
|
1468
|
+
const definitions = (root.$defs ??= {});
|
|
1469
|
+
const rewrittenRefs = new Map<string, string>();
|
|
1470
|
+
let aliasIndex = 0;
|
|
1471
|
+
|
|
1472
|
+
for (const ref of refsToPreserve) {
|
|
1473
|
+
const target = resolveLocalRef(root, ref);
|
|
1474
|
+
if (!isSchemaDefinition(target)) {
|
|
1475
|
+
throw new Error('Local $ref cannot be preserved before allOf property removal: ' + JSON.stringify(ref));
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
let alias = '__openai_strict_allOf_property_ref_' + aliasIndex++;
|
|
1479
|
+
while (Object.prototype.hasOwnProperty.call(definitions, alias)) {
|
|
1480
|
+
alias = '__openai_strict_allOf_property_ref_' + aliasIndex++;
|
|
1481
|
+
}
|
|
1482
|
+
definitions[alias] = structuredClone(target);
|
|
1483
|
+
rewrittenRefs.set(ref, '#/$defs/' + escapeJSONPointerToken(alias));
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const rewriteRefs = (value: JSONSchemaDefinition): void => {
|
|
1487
|
+
if (typeof value === 'boolean' || !isObject(value)) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (typeof value.$ref === 'string') {
|
|
1492
|
+
value.$ref = rewrittenRefs.get(value.$ref) ?? value.$ref;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
forEachJSONSchemaChild(value, [], (child) => {
|
|
1496
|
+
rewriteRefs(child as JSONSchemaDefinition);
|
|
1497
|
+
});
|
|
1498
|
+
};
|
|
1499
|
+
rewriteRefs(root);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function validateRefSchemas(schema: JSONSchemaDefinition, path: string[], root: JSONSchema): void {
|
|
1503
|
+
if (typeof schema === 'boolean' || !isObject(schema)) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const ref = schema.$ref;
|
|
1508
|
+
if (ref !== undefined) {
|
|
1509
|
+
if (typeof ref !== 'string') {
|
|
1510
|
+
throw new TypeError(`Received non-string $ref - ${ref}; path=${path.join('/')}`);
|
|
1511
|
+
}
|
|
1512
|
+
if (!ref.startsWith('#')) {
|
|
1513
|
+
throw new Error(
|
|
1514
|
+
`External $ref at \`${
|
|
1515
|
+
path.join('/') || '<root>'
|
|
1516
|
+
}\` is not supported in strict Structured Outputs: ${JSON.stringify(ref)}`,
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
const resolved = resolveLocalRef(root, ref);
|
|
1520
|
+
if (resolved === undefined || !isSchemaDefinition(resolved)) {
|
|
1521
|
+
throw new Error(
|
|
1522
|
+
`Local $ref at \`${
|
|
1523
|
+
path.join('/') || '<root>'
|
|
1524
|
+
}\` does not resolve to an object or boolean schema: ${JSON.stringify(ref)}`,
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
if (typeof resolved === 'boolean') {
|
|
1528
|
+
throw new TypeError(`Expected object schema but got boolean; path=${path.join('/')}`);
|
|
1529
|
+
}
|
|
1530
|
+
if (!hasOnlyRefAndAnnotations(schema)) {
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
`Schema $ref at \`${
|
|
1533
|
+
path.join('/') || '<root>'
|
|
1534
|
+
}\` has non-annotation siblings that Draft 7 ignores and cannot be represented in strict Structured Outputs.`,
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
forEachJSONSchemaChild(schema, path, (child, childPath) => {
|
|
1540
|
+
validateRefSchemas(child as JSONSchemaDefinition, childPath, root);
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
type ResolvedObjectAllOfBranch = {
|
|
1545
|
+
schema: JSONSchema;
|
|
1546
|
+
refChain: JSONSchema[];
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Resolve only local aliases whose siblings carry no validation semantics.
|
|
1551
|
+
* Keeping this separate from general ref validation lets allOf merging inspect
|
|
1552
|
+
* the effective object shape without broadening which refs or sibling
|
|
1553
|
+
* constraints are accepted.
|
|
1554
|
+
*/
|
|
1555
|
+
function resolveObjectAllOfBranch(
|
|
1556
|
+
schema: JSONSchema,
|
|
1557
|
+
root: JSONSchema,
|
|
1558
|
+
normalizing: Set<JSONSchema>,
|
|
1559
|
+
): ResolvedObjectAllOfBranch | undefined {
|
|
1560
|
+
const refChain = [schema];
|
|
1561
|
+
const seenRefs = new Set<string>();
|
|
1562
|
+
let resolved = schema;
|
|
1563
|
+
let resolvedPath: string[] = [];
|
|
1564
|
+
|
|
1565
|
+
while (true) {
|
|
1566
|
+
while (resolved.$ref !== undefined) {
|
|
1567
|
+
const ref = resolved.$ref;
|
|
1568
|
+
if (typeof ref !== 'string' || !hasOnlyRefAndAnnotations(resolved) || seenRefs.has(ref)) {
|
|
1569
|
+
return undefined;
|
|
1570
|
+
}
|
|
1571
|
+
seenRefs.add(ref);
|
|
1572
|
+
|
|
1573
|
+
const target = resolveLocalRef(root, ref);
|
|
1574
|
+
if (typeof target === 'boolean' || !isObject(target)) {
|
|
1575
|
+
return undefined;
|
|
1576
|
+
}
|
|
1577
|
+
const targetPath = parseLocalRef(ref);
|
|
1578
|
+
if (targetPath === undefined) {
|
|
1579
|
+
return undefined;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
resolved = target;
|
|
1583
|
+
resolvedPath = targetPath;
|
|
1584
|
+
refChain.push(resolved);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// A ref can point forward to a target whose own mergeable allOf has not
|
|
1588
|
+
// been visited yet. Normalize that target before its consumer classifies
|
|
1589
|
+
// the resolved shape, making property order irrelevant. An active target
|
|
1590
|
+
// is a ref/allOf cycle; leave it wrapped so the caller still fails closed.
|
|
1591
|
+
if (resolved.allOf !== undefined && !normalizing.has(resolved)) {
|
|
1592
|
+
const previousAllOf = resolved.allOf;
|
|
1593
|
+
normalizeObjectAllOfBranches(resolved, resolvedPath, root, normalizing);
|
|
1594
|
+
// Singleton flattening can expose another local ref. Follow it through
|
|
1595
|
+
// the same guarded loop before returning the effective branch.
|
|
1596
|
+
if (resolved.$ref !== undefined || resolved.allOf !== previousAllOf) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
return { schema: resolved, refChain };
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function normalizeObjectAllOfBranches(
|
|
1606
|
+
schema: JSONSchemaDefinition,
|
|
1607
|
+
path: string[],
|
|
1608
|
+
root: JSONSchema,
|
|
1609
|
+
normalizing: Set<JSONSchema> = new Set(),
|
|
1610
|
+
): void {
|
|
1611
|
+
if (typeof schema === 'boolean' || !isObject(schema)) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (normalizing.has(schema)) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
normalizing.add(schema);
|
|
1619
|
+
try {
|
|
1620
|
+
while (true) {
|
|
1621
|
+
forEachJSONSchemaChild(schema, path, (child, childPath) => {
|
|
1622
|
+
normalizeObjectAllOfBranches(child as JSONSchemaDefinition, childPath, root, normalizing);
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// Intersections are associative, so normalize nested object
|
|
1626
|
+
// intersections before classifying their parents. Otherwise an inner
|
|
1627
|
+
// allOf wrapper has no directly visible object shape and makes an
|
|
1628
|
+
// exactly mergeable outer allOf fail closed.
|
|
1629
|
+
if (!mergeObjectAllOf(schema, path, root, normalizing)) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
} finally {
|
|
1634
|
+
normalizing.delete(schema);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Standard Schema needs to prove oneOf branches exclusive before it rewrites
|
|
1640
|
+
* them to anyOf. Inspect a cloned branch through the same conservative object
|
|
1641
|
+
* allOf merger without mutating the caller's schema or broadening failures.
|
|
1642
|
+
*/
|
|
1643
|
+
export function normalizeObjectAllOfForExclusivity(
|
|
1644
|
+
schema: JSONSchema,
|
|
1645
|
+
root: JSONSchema,
|
|
1646
|
+
): JSONSchema | undefined {
|
|
1647
|
+
if (schema.allOf === undefined) {
|
|
1648
|
+
return undefined;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
const normalized = structuredClone(schema);
|
|
1652
|
+
try {
|
|
1653
|
+
// Removing neutral branches or flattening a singleton can expose another
|
|
1654
|
+
// mergeable allOf wrapper. Keep applying the recursive merger and safe
|
|
1655
|
+
// singleton flattening until the proof sees the branch's final shape.
|
|
1656
|
+
while (normalized.allOf !== undefined) {
|
|
1657
|
+
normalizeObjectAllOfBranches(normalized, [], root);
|
|
1658
|
+
if (normalized.allOf === undefined) {
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const allOf = normalized.allOf;
|
|
1663
|
+
if (!Array.isArray(allOf) || allOf.length !== 1 || !hasOnlyAnnotationSiblings(normalized, 'allOf')) {
|
|
1664
|
+
return undefined;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const branch = allOf[0];
|
|
1668
|
+
if (typeof branch === 'boolean' || !isObject(branch)) {
|
|
1669
|
+
return undefined;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const siblings = { ...normalized };
|
|
1673
|
+
delete siblings.allOf;
|
|
1674
|
+
const flattened = structuredClone(branch);
|
|
1675
|
+
for (const keyword of Object.keys(normalized)) {
|
|
1676
|
+
delete (normalized as Record<string, unknown>)[keyword];
|
|
1677
|
+
}
|
|
1678
|
+
Object.assign(normalized, flattened, siblings);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
return normalized;
|
|
1682
|
+
} catch {
|
|
1683
|
+
return undefined;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function mergeObjectAllOf(
|
|
1688
|
+
jsonSchema: JSONSchema,
|
|
1689
|
+
path: string[],
|
|
1690
|
+
root: JSONSchema,
|
|
1691
|
+
normalizing: Set<JSONSchema> = new Set(),
|
|
1692
|
+
): boolean {
|
|
1693
|
+
const allOf = jsonSchema.allOf;
|
|
1694
|
+
if (!Array.isArray(allOf) || allOf.length === 0) {
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Intersections are idempotent. Collapse structurally identical branches
|
|
1699
|
+
// before object-shape classification so duplicate scalar and array schemas
|
|
1700
|
+
// can reach the existing safe singleton-flattening path.
|
|
1701
|
+
const uniqueBranches = allOf.filter(
|
|
1702
|
+
(branch, index) => !allOf.slice(0, index).some((candidate) => schemasEqual(candidate, branch)),
|
|
1703
|
+
);
|
|
1704
|
+
if (uniqueBranches.length !== allOf.length) {
|
|
1705
|
+
jsonSchema.allOf = uniqueBranches;
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// `true` is the identity element for Draft 7 intersections. Remove it
|
|
1710
|
+
// before deciding whether the remaining branches are object-mergeable so a
|
|
1711
|
+
// neutral branch cannot make an otherwise exact merge fail closed.
|
|
1712
|
+
const nonNeutralBranches = allOf.filter((entry) => entry !== true);
|
|
1713
|
+
if (nonNeutralBranches.length !== allOf.length) {
|
|
1714
|
+
if (nonNeutralBranches.length === 0) {
|
|
1715
|
+
delete jsonSchema.allOf;
|
|
1716
|
+
} else {
|
|
1717
|
+
jsonSchema.allOf = nonNeutralBranches;
|
|
1718
|
+
}
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const parentHasObjectShape = hasObjectShapeWithoutAllOf(jsonSchema);
|
|
1723
|
+
const resolvedEntries = allOf.map((entry) =>
|
|
1724
|
+
isObject(entry) ? resolveObjectAllOfBranch(entry, root, normalizing) : undefined,
|
|
1725
|
+
);
|
|
1726
|
+
const objectBranches = resolvedEntries
|
|
1727
|
+
.map((entry) => entry?.schema)
|
|
1728
|
+
.filter((entry): entry is JSONSchema => entry !== undefined && hasObjectShapeWithoutAllOf(entry));
|
|
1729
|
+
if (!parentHasObjectShape && objectBranches.length === 0) {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
// A lone object branch with no object-valued parent is handled by the
|
|
1733
|
+
// existing safe single-allOf flattening path below.
|
|
1734
|
+
if (!parentHasObjectShape && allOf.length === 1) {
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const fail = (): never => {
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
`Object allOf at \`${
|
|
1741
|
+
path.join('/') || '<root>'
|
|
1742
|
+
}\` cannot be merged without changing Draft 7 validation.`,
|
|
1743
|
+
);
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
if (
|
|
1747
|
+
!parentHasObjectShape &&
|
|
1748
|
+
['additionalProperties', 'properties', 'required', 'type'].some((keyword) => keyword in jsonSchema)
|
|
1749
|
+
) {
|
|
1750
|
+
fail();
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
for (const keyword of Object.keys(jsonSchema)) {
|
|
1754
|
+
if (
|
|
1755
|
+
keyword !== 'allOf' &&
|
|
1756
|
+
keyword !== '$defs' &&
|
|
1757
|
+
keyword !== 'definitions' &&
|
|
1758
|
+
!(path.length === 0 && JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword)) &&
|
|
1759
|
+
!MERGEABLE_OBJECT_ALL_OF_KEYWORDS.has(keyword)
|
|
1760
|
+
) {
|
|
1761
|
+
fail();
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const branches: Array<{ schema: JSONSchema; sourcePath: string[] | undefined }> = [];
|
|
1766
|
+
if (parentHasObjectShape) {
|
|
1767
|
+
branches.push({ schema: jsonSchema, sourcePath: path });
|
|
1768
|
+
}
|
|
1769
|
+
for (const [index, entry] of allOf.entries()) {
|
|
1770
|
+
if (!isObject(entry)) {
|
|
1771
|
+
fail();
|
|
1772
|
+
}
|
|
1773
|
+
const resolvedEntry = resolvedEntries[index];
|
|
1774
|
+
if (resolvedEntry === undefined) {
|
|
1775
|
+
return fail();
|
|
1776
|
+
}
|
|
1777
|
+
const branch = resolvedEntry.schema;
|
|
1778
|
+
// Definition maps do not constrain instances. Any refs into an allOf
|
|
1779
|
+
// branch were preserved under stable root definitions before this merge,
|
|
1780
|
+
// so a valid definitions-only branch can now be discarded like an
|
|
1781
|
+
// annotation-only branch.
|
|
1782
|
+
if (hasObjectShapeWithoutAllOf(branch)) {
|
|
1783
|
+
branches.push({
|
|
1784
|
+
schema: branch,
|
|
1785
|
+
sourcePath: branch === entry ? [...path, 'allOf', String(index)] : undefined,
|
|
1786
|
+
});
|
|
1787
|
+
} else if (!hasOnlyNeutralAllOfBranchKeywords(branch)) {
|
|
1788
|
+
fail();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
const merged: JSONSchema = {};
|
|
1793
|
+
for (const keyword of ['$defs', 'definitions'] as const) {
|
|
1794
|
+
if (jsonSchema[keyword] !== undefined) {
|
|
1795
|
+
merged[keyword] = jsonSchema[keyword];
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (path.length === 0) {
|
|
1799
|
+
for (const keyword of JSON_SCHEMA_ROOT_METADATA_KEYWORDS) {
|
|
1800
|
+
if (keyword in jsonSchema) {
|
|
1801
|
+
(merged as Record<string, unknown>)[keyword] = (jsonSchema as Record<string, unknown>)[keyword];
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const mergedProperties = Object.create(null) as Record<string, JSONSchemaDefinition>;
|
|
1807
|
+
const mergedRequired = new Set<string>();
|
|
1808
|
+
const closedPropertySets: Set<string>[] = [];
|
|
1809
|
+
const propertyEntries: Array<{
|
|
1810
|
+
key: string;
|
|
1811
|
+
propertySchema: JSONSchemaDefinition;
|
|
1812
|
+
sourcePath: string[] | undefined;
|
|
1813
|
+
}> = [];
|
|
1814
|
+
let sawProperties = false;
|
|
1815
|
+
let sawRequired = false;
|
|
1816
|
+
let hasExplicitObjectType = false;
|
|
1817
|
+
let hasExplicitNullableObjectType = false;
|
|
1818
|
+
|
|
1819
|
+
const mergeAnnotations = (schema: JSONSchema) => {
|
|
1820
|
+
for (const keyword of JSON_SCHEMA_ANNOTATION_KEYWORDS) {
|
|
1821
|
+
if (!(keyword in schema)) continue;
|
|
1822
|
+
// Annotation keywords do not affect Draft 7 validation. Preserve the
|
|
1823
|
+
// first value (the outer schema, then earlier branches) instead of
|
|
1824
|
+
// rejecting an otherwise exactly mergeable intersection.
|
|
1825
|
+
if (!(keyword in merged)) {
|
|
1826
|
+
(merged as any)[keyword] = (schema as any)[keyword];
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
mergeAnnotations(jsonSchema);
|
|
1832
|
+
for (const resolvedEntry of resolvedEntries) {
|
|
1833
|
+
if (resolvedEntry === undefined) continue;
|
|
1834
|
+
for (const entry of resolvedEntry.refChain) {
|
|
1835
|
+
mergeAnnotations(entry);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
for (const { schema: branch, sourcePath } of branches) {
|
|
1840
|
+
for (const keyword of Object.keys(branch)) {
|
|
1841
|
+
if (keyword === 'allOf' && branch === jsonSchema) continue;
|
|
1842
|
+
if (
|
|
1843
|
+
(keyword === '$defs' || keyword === 'definitions') &&
|
|
1844
|
+
isObject((branch as Record<string, unknown>)[keyword])
|
|
1845
|
+
) {
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
if (branch === jsonSchema && path.length === 0 && JSON_SCHEMA_ROOT_METADATA_KEYWORDS.has(keyword)) {
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
if (!MERGEABLE_OBJECT_ALL_OF_KEYWORDS.has(keyword)) {
|
|
1852
|
+
fail();
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if (branch.type !== undefined) {
|
|
1857
|
+
if (!isMergeableObjectType(branch.type)) {
|
|
1858
|
+
fail();
|
|
1859
|
+
}
|
|
1860
|
+
if (branch.type === 'object') {
|
|
1861
|
+
hasExplicitObjectType = true;
|
|
1862
|
+
} else {
|
|
1863
|
+
hasExplicitNullableObjectType = true;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (branch.properties !== undefined) {
|
|
1868
|
+
if (!isObject(branch.properties)) {
|
|
1869
|
+
fail();
|
|
1870
|
+
}
|
|
1871
|
+
sawProperties = true;
|
|
1872
|
+
for (const [key, propertySchema] of Object.entries(branch.properties)) {
|
|
1873
|
+
propertyEntries.push({
|
|
1874
|
+
key,
|
|
1875
|
+
propertySchema,
|
|
1876
|
+
sourcePath: sourcePath === undefined ? undefined : [...sourcePath, 'properties', key],
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
if (branch.required !== undefined) {
|
|
1882
|
+
if (!Array.isArray(branch.required) || branch.required.some((key) => typeof key !== 'string')) {
|
|
1883
|
+
fail();
|
|
1884
|
+
}
|
|
1885
|
+
sawRequired = true;
|
|
1886
|
+
for (const key of branch.required) mergedRequired.add(key);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if ('additionalProperties' in branch) {
|
|
1890
|
+
if (branch.additionalProperties !== false) {
|
|
1891
|
+
fail();
|
|
1892
|
+
}
|
|
1893
|
+
closedPropertySets.push(new Set(Object.keys(branch.properties ?? {})));
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// A closed branch forbids every property it does not declare. Intersect all
|
|
1898
|
+
// closed property sets before merging schemas so optional declarations
|
|
1899
|
+
// excluded by another closed branch are discarded, while required excluded
|
|
1900
|
+
// properties remain unrepresentable and fail closed.
|
|
1901
|
+
const allowedClosedProperties =
|
|
1902
|
+
closedPropertySets.length === 0 ?
|
|
1903
|
+
undefined
|
|
1904
|
+
: closedPropertySets
|
|
1905
|
+
.slice(1)
|
|
1906
|
+
.reduce(
|
|
1907
|
+
(allowed, keys) => new Set([...allowed].filter((key) => keys.has(key))),
|
|
1908
|
+
new Set(closedPropertySets[0]),
|
|
1909
|
+
);
|
|
1910
|
+
const excludesRequiredProperty =
|
|
1911
|
+
allowedClosedProperties !== undefined &&
|
|
1912
|
+
[...mergedRequired].some((key) => !allowedClosedProperties.has(key));
|
|
1913
|
+
const collapsesToNull = excludesRequiredProperty && !hasExplicitObjectType && hasExplicitNullableObjectType;
|
|
1914
|
+
const discardedPropertyPaths = propertyEntries
|
|
1915
|
+
.filter(
|
|
1916
|
+
({ key, sourcePath }) =>
|
|
1917
|
+
sourcePath !== undefined &&
|
|
1918
|
+
(collapsesToNull || (allowedClosedProperties !== undefined && !allowedClosedProperties.has(key))),
|
|
1919
|
+
)
|
|
1920
|
+
.map(({ sourcePath }) => sourcePath!);
|
|
1921
|
+
preserveDiscardedAllOfPropertyRefTargets(root, discardedPropertyPaths);
|
|
1922
|
+
if (jsonSchema === root && root.$defs !== undefined) {
|
|
1923
|
+
merged.$defs = root.$defs;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
if (excludesRequiredProperty) {
|
|
1927
|
+
// Object keywords do not constrain null. If every explicit object type
|
|
1928
|
+
// also admits null, the object portion is contradictory but null remains
|
|
1929
|
+
// an exact representation of the intersection.
|
|
1930
|
+
if (collapsesToNull) {
|
|
1931
|
+
merged.type = 'null';
|
|
1932
|
+
for (const keyword of Object.keys(jsonSchema)) {
|
|
1933
|
+
delete (jsonSchema as any)[keyword];
|
|
1934
|
+
}
|
|
1935
|
+
Object.assign(jsonSchema, merged);
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
fail();
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
for (const { key, propertySchema } of propertyEntries) {
|
|
1942
|
+
if (allowedClosedProperties !== undefined && !allowedClosedProperties.has(key)) {
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
if (
|
|
1946
|
+
Object.prototype.hasOwnProperty.call(mergedProperties, key) &&
|
|
1947
|
+
!schemasEqual(mergedProperties[key], propertySchema)
|
|
1948
|
+
) {
|
|
1949
|
+
fail();
|
|
1950
|
+
}
|
|
1951
|
+
mergedProperties[key] = propertySchema;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
if (hasExplicitObjectType || hasExplicitNullableObjectType) {
|
|
1955
|
+
merged.type = hasExplicitObjectType ? 'object' : ['object', 'null'];
|
|
1956
|
+
}
|
|
1957
|
+
if (sawProperties) merged.properties = Object.fromEntries(Object.entries(mergedProperties));
|
|
1958
|
+
if (sawRequired) merged.required = [...mergedRequired];
|
|
1959
|
+
if (closedPropertySets.length > 0) merged.additionalProperties = false;
|
|
1960
|
+
|
|
1961
|
+
for (const keyword of Object.keys(jsonSchema)) {
|
|
1962
|
+
delete (jsonSchema as any)[keyword];
|
|
1963
|
+
}
|
|
1964
|
+
Object.assign(jsonSchema, merged);
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function hasObjectShapeWithoutAllOf(schema: JSONSchema): boolean {
|
|
1969
|
+
if (schema.type !== undefined) {
|
|
1970
|
+
return isMergeableObjectType(schema.type);
|
|
1971
|
+
}
|
|
1972
|
+
return Object.keys(schema).some((keyword) => JSON_SCHEMA_OBJECT_KEYWORDS.has(keyword));
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function hasOnlyNeutralAllOfBranchKeywords(schema: JSONSchema): boolean {
|
|
1976
|
+
const schemaRecord = schema as Record<string, unknown>;
|
|
1977
|
+
return Object.keys(schema).every(
|
|
1978
|
+
(keyword) =>
|
|
1979
|
+
JSON_SCHEMA_ANNOTATION_KEYWORDS.has(keyword) ||
|
|
1980
|
+
((keyword === '$defs' || keyword === 'definitions') && isObject(schemaRecord[keyword])),
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
function isMergeableObjectType(type: JSONSchema['type']): boolean {
|
|
1985
|
+
return (
|
|
1986
|
+
type === 'object' ||
|
|
1987
|
+
(Array.isArray(type) && type.length === 2 && type.includes('object') && type.includes('null'))
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function schemasEqual(left: unknown, right: unknown): boolean {
|
|
1992
|
+
if (left === right) {
|
|
1993
|
+
return true;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
1997
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
1998
|
+
return false;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return left.every((value, index) => schemasEqual(value, right[index]));
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
if (typeof left !== 'object' || left === null || typeof right !== 'object' || right === null) {
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
const leftRecord = left as Record<string, unknown>;
|
|
2009
|
+
const rightRecord = right as Record<string, unknown>;
|
|
2010
|
+
const leftKeys = Object.keys(leftRecord).sort();
|
|
2011
|
+
const rightKeys = Object.keys(rightRecord).sort();
|
|
2012
|
+
|
|
2013
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
return leftKeys.every(
|
|
2018
|
+
(key, index) => key === rightKeys[index] && schemasEqual(leftRecord[key], rightRecord[key]),
|
|
2019
|
+
);
|
|
193
2020
|
}
|