@mastra/schema-compat 0.10.2-alpha.2

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.
@@ -0,0 +1,328 @@
1
+ import { jsonSchema } from 'ai';
2
+ import type { LanguageModelV1, Schema } from 'ai';
3
+ import { MockLanguageModelV1 } from 'ai/test';
4
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
5
+ import { z } from 'zod';
6
+ import { SchemaCompatLayer } from './schema-compatibility';
7
+ import { convertZodSchemaToAISDKSchema, convertSchemaToZod, applyCompatLayer } from './utils';
8
+
9
+ const mockModel = new MockLanguageModelV1({
10
+ modelId: 'test-model',
11
+ defaultObjectGenerationMode: 'json',
12
+ });
13
+
14
+ class MockSchemaCompatibility extends SchemaCompatLayer {
15
+ constructor(
16
+ model: LanguageModelV1,
17
+ private shouldApplyValue: boolean = true,
18
+ ) {
19
+ super(model);
20
+ }
21
+
22
+ shouldApply(): boolean {
23
+ return this.shouldApplyValue;
24
+ }
25
+
26
+ getSchemaTarget() {
27
+ return 'jsonSchema7' as const;
28
+ }
29
+
30
+ processZodType(value: z.ZodTypeAny): any {
31
+ if (value._def.typeName === 'ZodString') {
32
+ return z.string().describe('processed string');
33
+ }
34
+ return value;
35
+ }
36
+ }
37
+
38
+ describe('Builder Functions', () => {
39
+ describe('convertZodSchemaToAISDKSchema', () => {
40
+ it('should convert simple Zod schema to AI SDK schema', () => {
41
+ const zodSchema = z.object({
42
+ name: z.string(),
43
+ age: z.number(),
44
+ });
45
+
46
+ const result = convertZodSchemaToAISDKSchema(zodSchema);
47
+
48
+ expect(result).toHaveProperty('jsonSchema');
49
+ expect(result).toHaveProperty('validate');
50
+ expect(typeof result.validate).toBe('function');
51
+ });
52
+
53
+ it('should create schema with validation function', () => {
54
+ const zodSchema = z.object({
55
+ email: z.string().email(),
56
+ });
57
+
58
+ const result = convertZodSchemaToAISDKSchema(zodSchema);
59
+
60
+ expect(result.validate).toBeDefined();
61
+
62
+ const validResult = result.validate!({ email: 'test@example.com' });
63
+ expect(validResult.success).toBe(true);
64
+ if (validResult.success) {
65
+ expect(validResult.value).toEqual({ email: 'test@example.com' });
66
+ }
67
+
68
+ const invalidResult = result.validate!({ email: 'invalid-email' });
69
+ expect(invalidResult.success).toBe(false);
70
+ });
71
+
72
+ it('should handle custom targets', () => {
73
+ const zodSchema = z.object({
74
+ name: z.string(),
75
+ });
76
+
77
+ const result = convertZodSchemaToAISDKSchema(zodSchema, 'openApi3');
78
+
79
+ expect(result).toHaveProperty('jsonSchema');
80
+ expect(result).toHaveProperty('validate');
81
+ });
82
+
83
+ it('should handle complex nested schemas', () => {
84
+ const zodSchema = z.object({
85
+ user: z.object({
86
+ name: z.string(),
87
+ preferences: z.object({
88
+ theme: z.enum(['light', 'dark']),
89
+ notifications: z.boolean(),
90
+ }),
91
+ }),
92
+ tags: z.array(z.string()),
93
+ });
94
+
95
+ const result = convertZodSchemaToAISDKSchema(zodSchema);
96
+
97
+ expect(result).toHaveProperty('jsonSchema');
98
+ expect(result.jsonSchema).toHaveProperty('properties');
99
+ });
100
+ });
101
+
102
+ describe('convertSchemaToZod', () => {
103
+ it('should return Zod schema unchanged', () => {
104
+ const zodSchema = z.object({
105
+ name: z.string(),
106
+ });
107
+
108
+ const result = convertSchemaToZod(zodSchema);
109
+
110
+ expect(result).toBe(zodSchema);
111
+ });
112
+
113
+ it('should convert AI SDK schema to Zod', () => {
114
+ const aiSchema: Schema = jsonSchema({
115
+ type: 'object',
116
+ properties: {
117
+ name: { type: 'string' },
118
+ age: { type: 'number' },
119
+ },
120
+ required: ['name'],
121
+ });
122
+
123
+ const result = convertSchemaToZod(aiSchema);
124
+
125
+ expect(result).toBeInstanceOf(z.ZodType);
126
+ const parseResult = result.safeParse({ name: 'John', age: 30 });
127
+ expect(parseResult.success).toBe(true);
128
+ });
129
+
130
+ it('should handle complex JSON schema conversion', () => {
131
+ const complexSchema: Schema = jsonSchema({
132
+ type: 'object',
133
+ properties: {
134
+ user: {
135
+ type: 'object',
136
+ properties: {
137
+ name: { type: 'string' },
138
+ email: { type: 'string', format: 'email' },
139
+ },
140
+ required: ['name'],
141
+ },
142
+ tags: {
143
+ type: 'array',
144
+ items: { type: 'string' },
145
+ },
146
+ },
147
+ required: ['user'],
148
+ });
149
+
150
+ const result = convertSchemaToZod(complexSchema);
151
+
152
+ expect(result).toBeInstanceOf(z.ZodType);
153
+
154
+ const validData = {
155
+ user: { name: 'John', email: 'john@example.com' },
156
+ tags: ['tag1', 'tag2'],
157
+ };
158
+ const parseResult = result.safeParse(validData);
159
+ expect(parseResult.success).toBe(true);
160
+ });
161
+ });
162
+
163
+ describe('applyCompatLayer', () => {
164
+ let mockCompatibility: MockSchemaCompatibility;
165
+
166
+ beforeEach(() => {
167
+ mockCompatibility = new MockSchemaCompatibility(mockModel);
168
+ });
169
+
170
+ it('should process Zod object schema with compatibility', () => {
171
+ const zodSchema = z.object({
172
+ name: z.string(),
173
+ age: z.number(),
174
+ });
175
+
176
+ const result = applyCompatLayer({
177
+ schema: zodSchema,
178
+ compatLayers: [mockCompatibility],
179
+ mode: 'aiSdkSchema',
180
+ });
181
+
182
+ expect(result).toHaveProperty('jsonSchema');
183
+ expect(result).toHaveProperty('validate');
184
+ });
185
+
186
+ it('should process AI SDK schema with compatibility', () => {
187
+ const aiSchema: Schema = jsonSchema({
188
+ type: 'object',
189
+ properties: {
190
+ name: { type: 'string' },
191
+ },
192
+ });
193
+
194
+ const result = applyCompatLayer({
195
+ schema: aiSchema,
196
+ compatLayers: [mockCompatibility],
197
+ mode: 'jsonSchema',
198
+ });
199
+
200
+ expect(typeof result).toBe('object');
201
+ expect(result).toHaveProperty('type');
202
+ });
203
+
204
+ it('should handle object schema with string property', () => {
205
+ const stringSchema = z.object({ value: z.string() });
206
+
207
+ const result = applyCompatLayer({
208
+ schema: stringSchema,
209
+ compatLayers: [mockCompatibility],
210
+ mode: 'aiSdkSchema',
211
+ });
212
+
213
+ expect(result).toHaveProperty('jsonSchema');
214
+ expect(result).toHaveProperty('validate');
215
+ });
216
+
217
+ it('should return processed schema when compatibility applies', () => {
218
+ const zodSchema = z.object({
219
+ name: z.string(),
220
+ });
221
+
222
+ const result = applyCompatLayer({
223
+ schema: zodSchema,
224
+ compatLayers: [mockCompatibility],
225
+ mode: 'aiSdkSchema',
226
+ });
227
+
228
+ expect(result).toHaveProperty('jsonSchema');
229
+ expect(result).toHaveProperty('validate');
230
+ });
231
+
232
+ it('should return fallback when no compatibility applies', () => {
233
+ const nonApplyingCompatibility = new MockSchemaCompatibility(mockModel, false);
234
+ const zodSchema = z.object({
235
+ name: z.string(),
236
+ });
237
+
238
+ const result = applyCompatLayer({
239
+ schema: zodSchema,
240
+ compatLayers: [nonApplyingCompatibility],
241
+ mode: 'aiSdkSchema',
242
+ });
243
+
244
+ expect(result).toHaveProperty('jsonSchema');
245
+ expect(result).toHaveProperty('validate');
246
+ });
247
+
248
+ it('should handle jsonSchema mode', () => {
249
+ const zodSchema = z.object({
250
+ name: z.string(),
251
+ });
252
+
253
+ const result = applyCompatLayer({
254
+ schema: zodSchema,
255
+ compatLayers: [mockCompatibility],
256
+ mode: 'jsonSchema',
257
+ });
258
+
259
+ expect(typeof result).toBe('object');
260
+ expect(result).toHaveProperty('type');
261
+ });
262
+
263
+ it('should handle empty compatLayers array', () => {
264
+ const zodSchema = z.object({
265
+ name: z.string(),
266
+ });
267
+
268
+ const result = applyCompatLayer({
269
+ schema: zodSchema,
270
+ compatLayers: [],
271
+ mode: 'aiSdkSchema',
272
+ });
273
+
274
+ expect(result).toHaveProperty('jsonSchema');
275
+ expect(result).toHaveProperty('validate');
276
+ });
277
+
278
+ it('should convert non-object AI SDK schema correctly', () => {
279
+ const stringSchema: Schema = jsonSchema({ type: 'string' });
280
+
281
+ const result = applyCompatLayer({
282
+ schema: stringSchema,
283
+ compatLayers: [mockCompatibility],
284
+ mode: 'aiSdkSchema',
285
+ });
286
+
287
+ expect(result).toHaveProperty('jsonSchema');
288
+ expect(result).toHaveProperty('validate');
289
+
290
+ // Verify the schema structure shows the string was wrapped in a 'value' property
291
+ const resultSchema = (result as Schema).jsonSchema;
292
+ expect(resultSchema).toHaveProperty('type', 'object');
293
+ expect(resultSchema).toHaveProperty('properties');
294
+ expect(resultSchema.properties).toHaveProperty('value');
295
+
296
+ // Verify the string property has the description added by the mock compatibility layer
297
+ const valueProperty = resultSchema.properties!.value as any;
298
+ expect(valueProperty).toHaveProperty('description', 'processed string');
299
+ });
300
+
301
+ it('should handle complex schema with multiple compatLayers', () => {
302
+ const compat1 = new MockSchemaCompatibility(mockModel, false);
303
+ const compat2 = new MockSchemaCompatibility(mockModel, true);
304
+
305
+ vi.spyOn(compat1, 'processZodType');
306
+ vi.spyOn(compat2, 'processZodType');
307
+
308
+ const zodSchema = z.object({
309
+ name: z.string(),
310
+ settings: z.object({
311
+ theme: z.string(),
312
+ notifications: z.boolean(),
313
+ }),
314
+ });
315
+
316
+ const result = applyCompatLayer({
317
+ schema: zodSchema,
318
+ compatLayers: [compat1, compat2],
319
+ mode: 'aiSdkSchema',
320
+ });
321
+
322
+ expect(result).toHaveProperty('jsonSchema');
323
+ expect(result).toHaveProperty('validate');
324
+ expect(compat1.processZodType).not.toHaveBeenCalled();
325
+ expect(compat2.processZodType).toHaveBeenCalled();
326
+ });
327
+ });
328
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,218 @@
1
+ import { jsonSchema } from 'ai';
2
+ import type { Schema } from 'ai';
3
+ import type { JSONSchema7 } from 'json-schema';
4
+ import type { ZodSchema } from 'zod';
5
+ import { z } from 'zod';
6
+ import { convertJsonSchemaToZod } from 'zod-from-json-schema';
7
+ import type { JSONSchema as ZodFromJSONSchema_JSONSchema } from 'zod-from-json-schema';
8
+ import type { Targets } from 'zod-to-json-schema';
9
+ import { zodToJsonSchema } from 'zod-to-json-schema';
10
+ import type { SchemaCompatLayer } from './schema-compatibility';
11
+
12
+ /**
13
+ * Converts a Zod schema to an AI SDK Schema with validation support.
14
+ *
15
+ * This function mirrors the behavior of Vercel's AI SDK zod-schema utility but allows
16
+ * customization of the JSON Schema target format.
17
+ *
18
+ * @param zodSchema - The Zod schema to convert
19
+ * @param target - The JSON Schema target format (defaults to 'jsonSchema7')
20
+ * @returns An AI SDK Schema object with built-in validation
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { z } from 'zod';
25
+ * import { convertZodSchemaToAISDKSchema } from '@mastra/schema-compat';
26
+ *
27
+ * const userSchema = z.object({
28
+ * name: z.string(),
29
+ * age: z.number().min(0)
30
+ * });
31
+ *
32
+ * const aiSchema = convertZodSchemaToAISDKSchema(userSchema);
33
+ * ```
34
+ */
35
+ // mirrors https://github.com/vercel/ai/blob/main/packages/ui-utils/src/zod-schema.ts#L21 but with a custom target
36
+ export function convertZodSchemaToAISDKSchema(zodSchema: ZodSchema, target: Targets = 'jsonSchema7') {
37
+ return jsonSchema(
38
+ zodToJsonSchema(zodSchema, {
39
+ $refStrategy: 'none',
40
+ target,
41
+ }) as JSONSchema7,
42
+ {
43
+ validate: value => {
44
+ const result = zodSchema.safeParse(value);
45
+ return result.success ? { success: true, value: result.data } : { success: false, error: result.error };
46
+ },
47
+ },
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Checks if a value is a Zod type by examining its properties and methods.
53
+ *
54
+ * @param value - The value to check
55
+ * @returns True if the value is a Zod type, false otherwise
56
+ * @internal
57
+ */
58
+ function isZodType(value: unknown): value is z.ZodType {
59
+ // Check if it's a Zod schema by looking for common Zod properties and methods
60
+ return (
61
+ typeof value === 'object' &&
62
+ value !== null &&
63
+ '_def' in value &&
64
+ 'parse' in value &&
65
+ typeof (value as any).parse === 'function' &&
66
+ 'safeParse' in value &&
67
+ typeof (value as any).safeParse === 'function'
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Converts an AI SDK Schema or Zod schema to a Zod schema.
73
+ *
74
+ * If the input is already a Zod schema, it returns it unchanged.
75
+ * If the input is an AI SDK Schema, it extracts the JSON schema and converts it to Zod.
76
+ *
77
+ * @param schema - The schema to convert (AI SDK Schema or Zod schema)
78
+ * @returns A Zod schema equivalent of the input
79
+ * @throws Error if the conversion fails
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * import { jsonSchema } from 'ai';
84
+ * import { convertSchemaToZod } from '@mastra/schema-compat';
85
+ *
86
+ * const aiSchema = jsonSchema({
87
+ * type: 'object',
88
+ * properties: {
89
+ * name: { type: 'string' }
90
+ * }
91
+ * });
92
+ *
93
+ * const zodSchema = convertSchemaToZod(aiSchema);
94
+ * ```
95
+ */
96
+ export function convertSchemaToZod(schema: Schema | z.ZodSchema): z.ZodType {
97
+ if (isZodType(schema)) {
98
+ return schema;
99
+ } else {
100
+ const jsonSchemaToConvert = ('jsonSchema' in schema ? schema.jsonSchema : schema) as ZodFromJSONSchema_JSONSchema;
101
+ try {
102
+ return convertJsonSchemaToZod(jsonSchemaToConvert);
103
+ } catch (e: unknown) {
104
+ const errorMessage = `[Schema Builder] Failed to convert schema parameters to Zod. Original schema: ${JSON.stringify(jsonSchemaToConvert)}`;
105
+ console.error(errorMessage, e);
106
+ throw new Error(errorMessage + (e instanceof Error ? `\n${e.stack}` : '\nUnknown error object'));
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Processes a schema using provider compatibility layers and converts it to an AI SDK Schema.
113
+ *
114
+ * @param options - Configuration object for schema processing
115
+ * @param options.schema - The schema to process (AI SDK Schema or Zod object schema)
116
+ * @param options.compatLayers - Array of compatibility layers to try
117
+ * @param options.mode - Must be 'aiSdkSchema'
118
+ * @returns Processed schema as an AI SDK Schema
119
+ */
120
+ export function applyCompatLayer(options: {
121
+ schema: Schema | z.AnyZodObject;
122
+ compatLayers: SchemaCompatLayer[];
123
+ mode: 'aiSdkSchema';
124
+ }): Schema;
125
+
126
+ /**
127
+ * Processes a schema using provider compatibility layers and converts it to a JSON Schema.
128
+ *
129
+ * @param options - Configuration object for schema processing
130
+ * @param options.schema - The schema to process (AI SDK Schema or Zod object schema)
131
+ * @param options.compatLayers - Array of compatibility layers to try
132
+ * @param options.mode - Must be 'jsonSchema'
133
+ * @returns Processed schema as a JSONSchema7
134
+ */
135
+ export function applyCompatLayer(options: {
136
+ schema: Schema | z.AnyZodObject;
137
+ compatLayers: SchemaCompatLayer[];
138
+ mode: 'jsonSchema';
139
+ }): JSONSchema7;
140
+
141
+ /**
142
+ * Processes a schema using provider compatibility layers and converts it to the specified format.
143
+ *
144
+ * This function automatically applies the first matching compatibility layer from the provided
145
+ * list based on the model configuration. If no compatibility applies, it falls back to
146
+ * standard conversion.
147
+ *
148
+ * @param options - Configuration object for schema processing
149
+ * @param options.schema - The schema to process (AI SDK Schema or Zod object schema)
150
+ * @param options.compatLayers - Array of compatibility layers to try
151
+ * @param options.mode - Output format: 'jsonSchema' for JSONSchema7 or 'aiSdkSchema' for AI SDK Schema
152
+ * @returns Processed schema in the requested format
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * import { z } from 'zod';
157
+ * import { applyCompatLayer, OpenAISchemaCompatLayer, AnthropicSchemaCompatLayer } from '@mastra/schema-compat';
158
+ *
159
+ * const schema = z.object({
160
+ * query: z.string().email(),
161
+ * limit: z.number().min(1).max(100)
162
+ * });
163
+ *
164
+ * const compatLayers = [
165
+ * new OpenAISchemaCompatLayer(model),
166
+ * new AnthropicSchemaCompatLayer(model)
167
+ * ];
168
+ *
169
+ * const result = applyCompatLayer({
170
+ * schema,
171
+ * compatLayers,
172
+ * mode: 'aiSdkSchema'
173
+ * });
174
+ * ```
175
+ */
176
+ export function applyCompatLayer({
177
+ schema,
178
+ compatLayers,
179
+ mode,
180
+ }: {
181
+ schema: Schema | z.AnyZodObject;
182
+ compatLayers: SchemaCompatLayer[];
183
+ mode: 'jsonSchema' | 'aiSdkSchema';
184
+ }): JSONSchema7 | Schema {
185
+ let zodSchema: z.AnyZodObject;
186
+
187
+ if (!isZodType(schema)) {
188
+ // Convert Schema to ZodObject
189
+ const convertedSchema = convertSchemaToZod(schema);
190
+ if (convertedSchema instanceof z.ZodObject) {
191
+ zodSchema = convertedSchema;
192
+ } else {
193
+ // If it's not an object schema, wrap it in an object
194
+ zodSchema = z.object({ value: convertedSchema });
195
+ }
196
+ } else {
197
+ // Ensure it's a ZodObject
198
+ if (schema instanceof z.ZodObject) {
199
+ zodSchema = schema;
200
+ } else {
201
+ // Wrap non-object schemas in an object
202
+ zodSchema = z.object({ value: schema });
203
+ }
204
+ }
205
+
206
+ for (const compat of compatLayers) {
207
+ if (compat.shouldApply()) {
208
+ return mode === 'jsonSchema' ? compat.processToJSONSchema(zodSchema) : compat.processToAISDKSchema(zodSchema);
209
+ }
210
+ }
211
+
212
+ // If no compatibility applied, convert back to appropriate format
213
+ if (mode === 'jsonSchema') {
214
+ return zodToJsonSchema(zodSchema, { $refStrategy: 'none', target: 'jsonSchema7' }) as JSONSchema7;
215
+ } else {
216
+ return convertZodSchemaToAISDKSchema(zodSchema);
217
+ }
218
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.node.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules", "**/*.test.ts"]
5
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ },
7
+ });