transform.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.toStrictJsonSchema = toStrictJsonSchema;
  4. function toStrictJsonSchema(schema) {
  5. if (schema.type !== 'object') {
  6. throw new Error(`Root schema must have type: 'object' but got type: ${schema.type ? `'${schema.type}'` : 'undefined'}`);
  7. }
  8. const schemaCopy = structuredClone(schema);
  9. return ensureStrictJsonSchema(schemaCopy, [], schemaCopy);
  10. }
  11. function isNullable(schema) {
  12. if (typeof schema === 'boolean') {
  13. return false;
  14. }
  15. if (schema.type === 'null') {
  16. return true;
  17. }
  18. for (const oneOfVariant of schema.oneOf ?? []) {
  19. if (isNullable(oneOfVariant)) {
  20. return true;
  21. }
  22. }
  23. for (const allOfVariant of schema.anyOf ?? []) {
  24. if (isNullable(allOfVariant)) {
  25. return true;
  26. }
  27. }
  28. return false;
  29. }
  30. /**
  31. * Mutates the given JSON schema to ensure it conforms to the `strict` standard
  32. * that the API expects.
  33. */
  34. function ensureStrictJsonSchema(jsonSchema, path, root) {
  35. if (typeof jsonSchema === 'boolean') {
  36. throw new TypeError(`Expected object schema but got boolean; path=${path.join('/')}`);
  37. }
  38. if (!isObject(jsonSchema)) {
  39. throw new TypeError(`Expected ${JSON.stringify(jsonSchema)} to be an object; path=${path.join('/')}`);
  40. }
  41. // Handle $defs (non-standard but sometimes used)
  42. const defs = jsonSchema.$defs;
  43. if (isObject(defs)) {
  44. for (const [defName, defSchema] of Object.entries(defs)) {
  45. ensureStrictJsonSchema(defSchema, [...path, '$defs', defName], root);
  46. }
  47. }
  48. // Handle definitions (draft-04 style, deprecated in draft-07 but still used)
  49. const definitions = jsonSchema.definitions;
  50. if (isObject(definitions)) {
  51. for (const [definitionName, definitionSchema] of Object.entries(definitions)) {
  52. ensureStrictJsonSchema(definitionSchema, [...path, 'definitions', definitionName], root);
  53. }
  54. }
  55. // Add additionalProperties: false to object types
  56. const typ = jsonSchema.type;
  57. if (typ === 'object' && !('additionalProperties' in jsonSchema)) {
  58. jsonSchema.additionalProperties = false;
  59. }
  60. const required = jsonSchema.required ?? [];
  61. // Handle object properties
  62. const properties = jsonSchema.properties;
  63. if (isObject(properties)) {
  64. for (const [key, value] of Object.entries(properties)) {
  65. if (!isNullable(value) && !required.includes(key)) {
  66. 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`);
  67. }
  68. }
  69. jsonSchema.required = Object.keys(properties);
  70. jsonSchema.properties = Object.fromEntries(Object.entries(properties).map(([key, propSchema]) => [
  71. key,
  72. ensureStrictJsonSchema(propSchema, [...path, 'properties', key], root),
  73. ]));
  74. }
  75. // Handle arrays
  76. const items = jsonSchema.items;
  77. if (isObject(items)) {
  78. jsonSchema.items = ensureStrictJsonSchema(items, [...path, 'items'], root);
  79. }
  80. // Handle unions (anyOf)
  81. const anyOf = jsonSchema.anyOf;
  82. if (Array.isArray(anyOf)) {
  83. jsonSchema.anyOf = anyOf.map((variant, i) => ensureStrictJsonSchema(variant, [...path, 'anyOf', String(i)], root));
  84. }
  85. // Handle intersections (allOf)
  86. const allOf = jsonSchema.allOf;
  87. if (Array.isArray(allOf)) {
  88. if (allOf.length === 1) {
  89. const resolved = ensureStrictJsonSchema(allOf[0], [...path, 'allOf', '0'], root);
  90. Object.assign(jsonSchema, resolved);
  91. delete jsonSchema.allOf;
  92. }
  93. else {
  94. jsonSchema.allOf = allOf.map((entry, i) => ensureStrictJsonSchema(entry, [...path, 'allOf', String(i)], root));
  95. }
  96. }
  97. // Strip `null` defaults as there's no meaningful distinction
  98. if (jsonSchema.default === null) {
  99. delete jsonSchema.default;
  100. }
  101. // Handle $ref with additional properties
  102. const ref = jsonSchema.$ref;
  103. if (ref && hasMoreThanNKeys(jsonSchema, 1)) {
  104. if (typeof ref !== 'string') {
  105. throw new TypeError(`Received non-string $ref - ${ref}; path=${path.join('/')}`);
  106. }
  107. const resolved = resolveRef(root, ref);
  108. if (typeof resolved === 'boolean') {
  109. throw new Error(`Expected \`$ref: ${ref}\` to resolve to an object schema but got boolean`);
  110. }
  111. if (!isObject(resolved)) {
  112. throw new Error(`Expected \`$ref: ${ref}\` to resolve to an object but got ${JSON.stringify(resolved)}`);
  113. }
  114. // Properties from the json schema take priority over the ones on the `$ref`
  115. Object.assign(jsonSchema, { ...resolved, ...jsonSchema });
  116. delete jsonSchema.$ref;
  117. // Since the schema expanded from `$ref` might not have `additionalProperties: false` applied,
  118. // we call `ensureStrictJsonSchema` again to fix the inlined schema and ensure it's valid.
  119. return ensureStrictJsonSchema(jsonSchema, path, root);
  120. }
  121. return jsonSchema;
  122. }
  123. function resolveRef(root, ref) {
  124. if (!ref.startsWith('#/')) {
  125. throw new Error(`Unexpected $ref format ${JSON.stringify(ref)}; Does not start with #/`);
  126. }
  127. const pathParts = ref.slice(2).split('/');
  128. let resolved = root;
  129. for (const key of pathParts) {
  130. if (!isObject(resolved)) {
  131. throw new Error(`encountered non-object entry while resolving ${ref} - ${JSON.stringify(resolved)}`);
  132. }
  133. const value = resolved[key];
  134. if (value === undefined) {
  135. throw new Error(`Key ${key} not found while resolving ${ref}`);
  136. }
  137. resolved = value;
  138. }
  139. return resolved;
  140. }
  141. function isObject(obj) {
  142. return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
  143. }
  144. function hasMoreThanNKeys(obj, n) {
  145. let i = 0;
  146. for (const _ in obj) {
  147. i++;
  148. if (i > n) {
  149. return true;
  150. }
  151. }
  152. return false;
  153. }
  154. //# sourceMappingURL=transform.js.map