| import { z } from 'zod'; |
| import type { JsonSchemaType, ConvertJsonSchemaToZodOptions } from '@librechat/data-schemas'; |
|
|
| function isEmptyObjectSchema(jsonSchema?: JsonSchemaType): boolean { |
| return ( |
| jsonSchema != null && |
| typeof jsonSchema === 'object' && |
| jsonSchema.type === 'object' && |
| (jsonSchema.properties == null || Object.keys(jsonSchema.properties).length === 0) && |
| !jsonSchema.additionalProperties |
| ); |
| } |
|
|
| function dropSchemaFields( |
| schema: JsonSchemaType | undefined, |
| fields: string[], |
| ): JsonSchemaType | undefined { |
| if (schema == null || typeof schema !== 'object') { |
| return schema; |
| } |
| |
| if (Array.isArray(schema)) { |
| |
| return schema as unknown as JsonSchemaType; |
| } |
| const result: Record<string, unknown> = {}; |
| for (const [key, value] of Object.entries(schema)) { |
| if (fields.includes(key)) { |
| continue; |
| } |
| |
| if (key === 'items' || key === 'additionalProperties' || key === 'properties') { |
| if (key === 'properties' && value && typeof value === 'object') { |
| |
| const newProps: Record<string, JsonSchemaType> = {}; |
| for (const [propKey, propValue] of Object.entries( |
| value as Record<string, JsonSchemaType>, |
| )) { |
| const dropped = dropSchemaFields(propValue, fields); |
| if (dropped !== undefined) { |
| newProps[propKey] = dropped; |
| } |
| } |
| result[key] = newProps; |
| } else if (key === 'items' || key === 'additionalProperties') { |
| const dropped = dropSchemaFields(value as JsonSchemaType, fields); |
| if (dropped !== undefined) { |
| result[key] = dropped; |
| } |
| } |
| } else { |
| result[key] = value; |
| } |
| } |
| |
| if ( |
| typeof result.type === 'string' && |
| ['string', 'number', 'boolean', 'array', 'object'].includes(result.type) |
| ) { |
| return result as JsonSchemaType; |
| } |
| return undefined; |
| } |
|
|
| |
| function convertToZodUnion( |
| schemas: Record<string, unknown>[], |
| options: ConvertJsonSchemaToZodOptions, |
| ): z.ZodType | undefined { |
| if (!Array.isArray(schemas) || schemas.length === 0) { |
| return undefined; |
| } |
|
|
| |
| const zodSchemas = schemas |
| .map((subSchema) => { |
| |
| if (!subSchema.type && subSchema.properties) { |
| |
| const objSchema = { ...subSchema, type: 'object' } as JsonSchemaType; |
|
|
| |
| if (Array.isArray(subSchema.required) && subSchema.required.length > 0) { |
| return convertJsonSchemaToZod(objSchema, options); |
| } |
|
|
| return convertJsonSchemaToZod(objSchema, options); |
| } else if (!subSchema.type && subSchema.additionalProperties) { |
| |
| const objSchema = { ...subSchema, type: 'object' } as JsonSchemaType; |
| return convertJsonSchemaToZod(objSchema, options); |
| } else if (!subSchema.type && subSchema.items) { |
| |
| return convertJsonSchemaToZod({ ...subSchema, type: 'array' } as JsonSchemaType, options); |
| } else if (!subSchema.type && Array.isArray(subSchema.enum)) { |
| |
| return convertJsonSchemaToZod({ ...subSchema, type: 'string' } as JsonSchemaType, options); |
| } else if (!subSchema.type && subSchema.required) { |
| |
| |
| const objSchema = { |
| type: 'object', |
| properties: {}, |
| required: subSchema.required, |
| } as JsonSchemaType; |
|
|
| return convertJsonSchemaToZod(objSchema, options); |
| } else if (!subSchema.type && typeof subSchema === 'object') { |
| |
| |
|
|
| |
| if (subSchema.properties && Object.keys(subSchema.properties).length > 0) { |
| |
| const objSchema = { |
| type: 'object', |
| properties: subSchema.properties, |
| additionalProperties: true, |
| |
| } as JsonSchemaType; |
|
|
| |
| const zodSchema = convertJsonSchemaToZod(objSchema, options); |
|
|
| |
| if ('optional' in (subSchema.properties as Record<string, unknown>)) { |
| |
| const customSchema = z |
| .object({ |
| optional: z.boolean(), |
| }) |
| .passthrough(); |
|
|
| return customSchema; |
| } |
|
|
| if (zodSchema instanceof z.ZodObject) { |
| |
| return zodSchema.passthrough(); |
| } |
| return zodSchema; |
| } |
|
|
| |
| const objSchema = { |
| type: 'object', |
| ...subSchema, |
| } as JsonSchemaType; |
|
|
| return convertJsonSchemaToZod(objSchema, options); |
| } |
|
|
| |
| return convertJsonSchemaToZod(subSchema as JsonSchemaType, options); |
| }) |
| .filter((schema): schema is z.ZodType => schema !== undefined); |
|
|
| if (zodSchemas.length === 0) { |
| return undefined; |
| } |
|
|
| if (zodSchemas.length === 1) { |
| return zodSchemas[0]; |
| } |
|
|
| |
| if (zodSchemas.length >= 2) { |
| return z.union([zodSchemas[0], zodSchemas[1], ...zodSchemas.slice(2)]); |
| } |
|
|
| |
| return zodSchemas[0]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function resolveJsonSchemaRefs<T extends Record<string, unknown>>( |
| schema: T, |
| definitions?: Record<string, unknown>, |
| visited = new Set<string>(), |
| ): T { |
| |
| if (!schema || typeof schema !== 'object') { |
| return schema; |
| } |
|
|
| |
| if (!definitions) { |
| definitions = (schema.$defs || schema.definitions) as Record<string, unknown>; |
| } |
|
|
| |
| if (Array.isArray(schema)) { |
| return schema.map((item) => resolveJsonSchemaRefs(item, definitions, visited)) as unknown as T; |
| } |
|
|
| |
| const result: Record<string, unknown> = {}; |
|
|
| for (const [key, value] of Object.entries(schema)) { |
| |
| if ((key === '$defs' || key === 'definitions') && !visited.size) { |
| result[key] = value; |
| continue; |
| } |
|
|
| |
| if (key === '$ref' && typeof value === 'string') { |
| |
| if (visited.has(value)) { |
| |
| return { type: 'object' } as unknown as T; |
| } |
|
|
| |
| const refPath = value.replace(/^#\/(\$defs|definitions)\//, ''); |
| const resolved = definitions?.[refPath]; |
|
|
| if (resolved) { |
| visited.add(value); |
| const resolvedSchema = resolveJsonSchemaRefs( |
| resolved as Record<string, unknown>, |
| definitions, |
| visited, |
| ); |
| visited.delete(value); |
|
|
| |
| Object.assign(result, resolvedSchema); |
| } else { |
| |
| result[key] = value; |
| } |
| } else if (value && typeof value === 'object') { |
| |
| result[key] = resolveJsonSchemaRefs(value as Record<string, unknown>, definitions, visited); |
| } else { |
| |
| result[key] = value; |
| } |
| } |
|
|
| return result as T; |
| } |
|
|
| export function convertJsonSchemaToZod( |
| schema: JsonSchemaType & Record<string, unknown>, |
| options: ConvertJsonSchemaToZodOptions = {}, |
| ): z.ZodType | undefined { |
| const { allowEmptyObject = true, dropFields, transformOneOfAnyOf = false } = options; |
|
|
| |
| if (transformOneOfAnyOf) { |
| |
| if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) { |
| |
| |
| const hasOptionalProperty = schema.oneOf.some( |
| (subSchema) => |
| subSchema.properties && |
| typeof subSchema.properties === 'object' && |
| 'optional' in subSchema.properties, |
| ); |
|
|
| |
| if (schema.properties && Object.keys(schema.properties).length > 0) { |
| |
| const baseSchema = { ...schema }; |
| delete baseSchema.oneOf; |
|
|
| |
| const baseZodSchema = convertJsonSchemaToZod(baseSchema, { |
| ...options, |
| transformOneOfAnyOf: false, |
| }); |
|
|
| |
| const oneOfZodSchema = convertToZodUnion(schema.oneOf, options); |
|
|
| |
| if (baseZodSchema && oneOfZodSchema) { |
| |
| if (hasOptionalProperty) { |
| return z.union([baseZodSchema, oneOfZodSchema]); |
| } |
| |
| return z.intersection(baseZodSchema, oneOfZodSchema); |
| } |
| } |
|
|
| |
| return convertToZodUnion(schema.oneOf, options); |
| } |
|
|
| |
| if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) { |
| |
| if (schema.properties && Object.keys(schema.properties).length > 0) { |
| |
| const baseSchema = { ...schema }; |
| delete baseSchema.anyOf; |
|
|
| |
| const baseZodSchema = convertJsonSchemaToZod(baseSchema, { |
| ...options, |
| transformOneOfAnyOf: false, |
| }); |
|
|
| |
| const anyOfZodSchema = convertToZodUnion(schema.anyOf, options); |
|
|
| |
| if (baseZodSchema && anyOfZodSchema) { |
| |
| return z.intersection(baseZodSchema, anyOfZodSchema); |
| } |
| } |
|
|
| |
| return convertToZodUnion(schema.anyOf, options); |
| } |
|
|
| |
| } |
|
|
| if (dropFields && Array.isArray(dropFields) && dropFields.length > 0) { |
| const droppedSchema = dropSchemaFields(schema, dropFields); |
| if (!droppedSchema) { |
| return undefined; |
| } |
| schema = droppedSchema as JsonSchemaType & Record<string, unknown>; |
| } |
|
|
| if (!allowEmptyObject && isEmptyObjectSchema(schema)) { |
| return undefined; |
| } |
|
|
| let zodSchema: z.ZodType; |
|
|
| |
| if (schema.type === 'string') { |
| if (Array.isArray(schema.enum) && schema.enum.length > 0) { |
| const [first, ...rest] = schema.enum; |
| zodSchema = z.enum([first, ...rest] as [string, ...string[]]); |
| } else { |
| zodSchema = z.string(); |
| } |
| } else if (schema.type === 'number' || schema.type === 'integer' || schema.type === 'float') { |
| zodSchema = z.number(); |
| } else if (schema.type === 'boolean') { |
| zodSchema = z.boolean(); |
| } else if (schema.type === 'array' && schema.items !== undefined) { |
| const itemSchema = convertJsonSchemaToZod(schema.items as JsonSchemaType); |
| zodSchema = z.array((itemSchema ?? z.unknown()) as z.ZodType); |
| } else if (schema.type === 'object') { |
| const shape: Record<string, z.ZodType> = {}; |
| const properties = schema.properties ?? {}; |
|
|
| |
| |
| const isBareObjectSchema = |
| Object.keys(properties).length === 0 && |
| schema.additionalProperties === undefined && |
| !schema.patternProperties && |
| !schema.propertyNames && |
| !schema.$ref && |
| !schema.allOf && |
| !schema.anyOf && |
| !schema.oneOf; |
|
|
| for (const [key, value] of Object.entries(properties)) { |
| |
| if (transformOneOfAnyOf) { |
| const valueWithAny = value as JsonSchemaType & Record<string, unknown>; |
|
|
| |
| if (Array.isArray(valueWithAny.oneOf) && valueWithAny.oneOf.length > 0) { |
| |
| let fieldSchema = convertJsonSchemaToZod(valueWithAny, { |
| ...options, |
| transformOneOfAnyOf: true, |
| }); |
|
|
| if (!fieldSchema) { |
| continue; |
| } |
|
|
| if (value.description != null && value.description !== '') { |
| fieldSchema = fieldSchema.describe(value.description); |
| } |
|
|
| shape[key] = fieldSchema; |
| continue; |
| } |
|
|
| |
| if (Array.isArray(valueWithAny.anyOf) && valueWithAny.anyOf.length > 0) { |
| |
| let fieldSchema = convertJsonSchemaToZod(valueWithAny, { |
| ...options, |
| transformOneOfAnyOf: true, |
| }); |
|
|
| if (!fieldSchema) { |
| continue; |
| } |
|
|
| if (value.description != null && value.description !== '') { |
| fieldSchema = fieldSchema.describe(value.description); |
| } |
|
|
| shape[key] = fieldSchema; |
| continue; |
| } |
| } |
|
|
| |
| let fieldSchema = convertJsonSchemaToZod(value, options); |
| if (!fieldSchema) { |
| continue; |
| } |
| if (value.description != null && value.description !== '') { |
| fieldSchema = fieldSchema.describe(value.description); |
| } |
| shape[key] = fieldSchema; |
| } |
|
|
| let objectSchema = z.object(shape); |
|
|
| if (Array.isArray(schema.required) && schema.required.length > 0) { |
| const partial = Object.fromEntries( |
| Object.entries(shape).map(([key, value]) => [ |
| key, |
| schema.required?.includes(key) === true ? value : value.optional().nullable(), |
| ]), |
| ); |
| objectSchema = z.object(partial); |
| } else { |
| const partialNullable = Object.fromEntries( |
| Object.entries(shape).map(([key, value]) => [key, value.optional().nullable()]), |
| ); |
| objectSchema = z.object(partialNullable); |
| } |
|
|
| |
| if (schema.additionalProperties === true || isBareObjectSchema) { |
| |
| |
| zodSchema = objectSchema.passthrough(); |
| } else if (typeof schema.additionalProperties === 'object') { |
| |
| const additionalSchema = convertJsonSchemaToZod( |
| schema.additionalProperties as JsonSchemaType, |
| ); |
| zodSchema = objectSchema.catchall((additionalSchema ?? z.unknown()) as z.ZodType); |
| } else { |
| zodSchema = objectSchema; |
| } |
| } else { |
| zodSchema = z.unknown(); |
| } |
|
|
| |
| if (schema.description != null && schema.description !== '') { |
| zodSchema = zodSchema.describe(schema.description); |
| } |
|
|
| return zodSchema; |
| } |
|
|
| |
| |
| |
| |
| export function convertWithResolvedRefs( |
| schema: JsonSchemaType & Record<string, unknown>, |
| options?: ConvertJsonSchemaToZodOptions, |
| ) { |
| const resolved = resolveJsonSchemaRefs(schema); |
| return convertJsonSchemaToZod(resolved, options); |
| } |
|
|