2025-11-05 17:04:23 -03:00

154 lines
6.1 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.toStrictJsonSchema = toStrictJsonSchema;
function toStrictJsonSchema(schema) {
if (schema.type !== 'object') {
throw new Error(`Root schema must have type: 'object' but got type: ${schema.type ? `'${schema.type}'` : 'undefined'}`);
}
const schemaCopy = structuredClone(schema);
return ensureStrictJsonSchema(schemaCopy, [], schemaCopy);
}
function isNullable(schema) {
if (typeof schema === 'boolean') {
return false;
}
if (schema.type === 'null') {
return true;
}
for (const oneOfVariant of schema.oneOf ?? []) {
if (isNullable(oneOfVariant)) {
return true;
}
}
for (const allOfVariant of schema.anyOf ?? []) {
if (isNullable(allOfVariant)) {
return true;
}
}
return false;
}
/**
* Mutates the given JSON schema to ensure it conforms to the `strict` standard
* that the API expects.
*/
function ensureStrictJsonSchema(jsonSchema, path, root) {
if (typeof jsonSchema === 'boolean') {
throw new TypeError(`Expected object schema but got boolean; path=${path.join('/')}`);
}
if (!isObject(jsonSchema)) {
throw new TypeError(`Expected ${JSON.stringify(jsonSchema)} to be an object; path=${path.join('/')}`);
}
// Handle $defs (non-standard but sometimes used)
const defs = jsonSchema.$defs;
if (isObject(defs)) {
for (const [defName, defSchema] of Object.entries(defs)) {
ensureStrictJsonSchema(defSchema, [...path, '$defs', defName], root);
}
}
// Handle definitions (draft-04 style, deprecated in draft-07 but still used)
const definitions = jsonSchema.definitions;
if (isObject(definitions)) {
for (const [definitionName, definitionSchema] of Object.entries(definitions)) {
ensureStrictJsonSchema(definitionSchema, [...path, 'definitions', definitionName], root);
}
}
// Add additionalProperties: false to object types
const typ = jsonSchema.type;
if (typ === 'object' && !('additionalProperties' in jsonSchema)) {
jsonSchema.additionalProperties = false;
}
const required = jsonSchema.required ?? [];
// Handle object properties
const properties = jsonSchema.properties;
if (isObject(properties)) {
for (const [key, value] of Object.entries(properties)) {
if (!isNullable(value) && !required.includes(key)) {
throw new Error(`Zod field at \`${[...path, 'properties', key].join('/')}\` 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`);
}
}
jsonSchema.required = Object.keys(properties);
jsonSchema.properties = Object.fromEntries(Object.entries(properties).map(([key, propSchema]) => [
key,
ensureStrictJsonSchema(propSchema, [...path, 'properties', key], root),
]));
}
// Handle arrays
const items = jsonSchema.items;
if (isObject(items)) {
jsonSchema.items = ensureStrictJsonSchema(items, [...path, 'items'], root);
}
// Handle unions (anyOf)
const anyOf = jsonSchema.anyOf;
if (Array.isArray(anyOf)) {
jsonSchema.anyOf = anyOf.map((variant, i) => ensureStrictJsonSchema(variant, [...path, 'anyOf', String(i)], root));
}
// Handle intersections (allOf)
const allOf = jsonSchema.allOf;
if (Array.isArray(allOf)) {
if (allOf.length === 1) {
const resolved = ensureStrictJsonSchema(allOf[0], [...path, 'allOf', '0'], root);
Object.assign(jsonSchema, resolved);
delete jsonSchema.allOf;
}
else {
jsonSchema.allOf = allOf.map((entry, i) => ensureStrictJsonSchema(entry, [...path, 'allOf', String(i)], root));
}
}
// Strip `null` defaults as there's no meaningful distinction
if (jsonSchema.default === null) {
delete jsonSchema.default;
}
// Handle $ref with additional properties
const ref = jsonSchema.$ref;
if (ref && hasMoreThanNKeys(jsonSchema, 1)) {
if (typeof ref !== 'string') {
throw new TypeError(`Received non-string $ref - ${ref}; path=${path.join('/')}`);
}
const resolved = resolveRef(root, ref);
if (typeof resolved === 'boolean') {
throw new Error(`Expected \`$ref: ${ref}\` to resolve to an object schema but got boolean`);
}
if (!isObject(resolved)) {
throw new Error(`Expected \`$ref: ${ref}\` to resolve to an object but got ${JSON.stringify(resolved)}`);
}
// Properties from the json schema take priority over the ones on the `$ref`
Object.assign(jsonSchema, { ...resolved, ...jsonSchema });
delete jsonSchema.$ref;
// Since the schema expanded from `$ref` might not have `additionalProperties: false` applied,
// we call `ensureStrictJsonSchema` again to fix the inlined schema and ensure it's valid.
return ensureStrictJsonSchema(jsonSchema, path, root);
}
return jsonSchema;
}
function resolveRef(root, ref) {
if (!ref.startsWith('#/')) {
throw new Error(`Unexpected $ref format ${JSON.stringify(ref)}; Does not start with #/`);
}
const pathParts = ref.slice(2).split('/');
let resolved = root;
for (const key of pathParts) {
if (!isObject(resolved)) {
throw new Error(`encountered non-object entry while resolving ${ref} - ${JSON.stringify(resolved)}`);
}
const value = resolved[key];
if (value === undefined) {
throw new Error(`Key ${key} not found while resolving ${ref}`);
}
resolved = value;
}
return resolved;
}
function isObject(obj) {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
}
function hasMoreThanNKeys(obj, n) {
let i = 0;
for (const _ in obj) {
i++;
if (i > n) {
return true;
}
}
return false;
}
//# sourceMappingURL=transform.js.map