|
1 | | -import { describe, it, expect } from "vitest"; |
| 1 | +import { describe, it, expect, vi } from "vitest"; |
2 | 2 | import { z } from "zod"; |
3 | 3 | import { |
4 | 4 | convertJsonSchemaToZodSchema, |
@@ -191,6 +191,169 @@ describe("convertJsonSchemaToZodSchema", () => { |
191 | 191 |
|
192 | 192 | expect(resultSchemaJson).toStrictEqual(expectedSchemaJson); |
193 | 193 | }); |
| 194 | + |
| 195 | + it("should resolve non-circular $ref definitions correctly", () => { |
| 196 | + const jsonSchema = { |
| 197 | + type: "object", |
| 198 | + properties: { |
| 199 | + address: { $ref: "#/$defs/Address" }, |
| 200 | + }, |
| 201 | + required: ["address"], |
| 202 | + $defs: { |
| 203 | + Address: { |
| 204 | + type: "object", |
| 205 | + properties: { |
| 206 | + street: { type: "string", description: "Street name" }, |
| 207 | + city: { type: "string", description: "City name" }, |
| 208 | + }, |
| 209 | + required: ["street", "city"], |
| 210 | + description: "A postal address", |
| 211 | + }, |
| 212 | + }, |
| 213 | + }; |
| 214 | + |
| 215 | + const result = convertJsonSchemaToZodSchema(jsonSchema, true); |
| 216 | + const resultJson = zodToJsonSchema(result); |
| 217 | + |
| 218 | + const expectedSchema = z.object({ |
| 219 | + address: z |
| 220 | + .object({ |
| 221 | + street: z.string().describe("Street name"), |
| 222 | + city: z.string().describe("City name"), |
| 223 | + }) |
| 224 | + .describe("A postal address"), |
| 225 | + }); |
| 226 | + const expectedJson = zodToJsonSchema(expectedSchema); |
| 227 | + |
| 228 | + expect(resultJson).toStrictEqual(expectedJson); |
| 229 | + }); |
| 230 | + |
| 231 | + it("should handle circular $ref without crashing and return z.any()", () => { |
| 232 | + // A schema where Node references itself — this would cause infinite |
| 233 | + // recursion without cycle detection. |
| 234 | + const jsonSchema = { |
| 235 | + type: "object", |
| 236 | + properties: { |
| 237 | + root: { $ref: "#/$defs/Node" }, |
| 238 | + }, |
| 239 | + required: ["root"], |
| 240 | + $defs: { |
| 241 | + Node: { |
| 242 | + type: "object", |
| 243 | + properties: { |
| 244 | + value: { type: "string", description: "Node value" }, |
| 245 | + child: { $ref: "#/$defs/Node" }, |
| 246 | + }, |
| 247 | + required: ["value"], |
| 248 | + description: "A tree node", |
| 249 | + }, |
| 250 | + }, |
| 251 | + }; |
| 252 | + |
| 253 | + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); |
| 254 | + |
| 255 | + // Must not throw or hang |
| 256 | + const result = convertJsonSchemaToZodSchema(jsonSchema, true); |
| 257 | + expect(result).toBeDefined(); |
| 258 | + |
| 259 | + // The circular ref should have produced a console.warn |
| 260 | + expect(warnSpy).toHaveBeenCalledWith( |
| 261 | + expect.stringContaining("Circular $ref detected"), |
| 262 | + ); |
| 263 | + |
| 264 | + // The top-level shape should still have a "root" key that is an object |
| 265 | + const shape = (result as z.ZodObject<any>).shape; |
| 266 | + expect(shape.root).toBeDefined(); |
| 267 | + |
| 268 | + // Inside root, "value" should be a string and "child" should be z.any() |
| 269 | + // (child is optional since it's not in required[], so unwrap ZodOptional) |
| 270 | + const rootShape = (shape.root as z.ZodObject<any>).shape; |
| 271 | + expect(rootShape.value._def.typeName).toBe("ZodString"); |
| 272 | + const childDef = rootShape.child._def; |
| 273 | + if (childDef.typeName === "ZodOptional") { |
| 274 | + expect(childDef.innerType._def.typeName).toBe("ZodAny"); |
| 275 | + } else { |
| 276 | + expect(childDef.typeName).toBe("ZodAny"); |
| 277 | + } |
| 278 | + |
| 279 | + warnSpy.mockRestore(); |
| 280 | + }); |
| 281 | + |
| 282 | + it("should handle anyOf with $ref variants", () => { |
| 283 | + const jsonSchema = { |
| 284 | + type: "object", |
| 285 | + properties: { |
| 286 | + pet: { |
| 287 | + anyOf: [{ $ref: "#/$defs/Cat" }, { $ref: "#/$defs/Dog" }], |
| 288 | + description: "A pet", |
| 289 | + }, |
| 290 | + }, |
| 291 | + required: ["pet"], |
| 292 | + $defs: { |
| 293 | + Cat: { |
| 294 | + type: "object", |
| 295 | + properties: { meow: { type: "boolean" } }, |
| 296 | + required: ["meow"], |
| 297 | + }, |
| 298 | + Dog: { |
| 299 | + type: "object", |
| 300 | + properties: { bark: { type: "boolean" } }, |
| 301 | + required: ["bark"], |
| 302 | + }, |
| 303 | + }, |
| 304 | + }; |
| 305 | + |
| 306 | + const result = convertJsonSchemaToZodSchema(jsonSchema, true); |
| 307 | + expect(result).toBeDefined(); |
| 308 | + |
| 309 | + // Should produce a union inside the "pet" property |
| 310 | + const petSchema = (result as z.ZodObject<any>).shape.pet; |
| 311 | + expect(petSchema._def.typeName).toBe("ZodUnion"); |
| 312 | + }); |
| 313 | + |
| 314 | + it("should handle integer type as z.number()", () => { |
| 315 | + const jsonSchema = { |
| 316 | + type: "object", |
| 317 | + properties: { |
| 318 | + count: { type: "integer", description: "A count" }, |
| 319 | + }, |
| 320 | + required: ["count"], |
| 321 | + }; |
| 322 | + |
| 323 | + const result = convertJsonSchemaToZodSchema(jsonSchema, true); |
| 324 | + const shape = (result as z.ZodObject<any>).shape; |
| 325 | + expect(shape.count._def.typeName).toBe("ZodNumber"); |
| 326 | + }); |
| 327 | + |
| 328 | + it("should handle null type", () => { |
| 329 | + const jsonSchema = { |
| 330 | + type: "object", |
| 331 | + properties: { |
| 332 | + empty: { type: "null", description: "Always null" }, |
| 333 | + }, |
| 334 | + required: ["empty"], |
| 335 | + }; |
| 336 | + |
| 337 | + const result = convertJsonSchemaToZodSchema(jsonSchema, true); |
| 338 | + const shape = (result as z.ZodObject<any>).shape; |
| 339 | + expect(shape.empty._def.typeName).toBe("ZodNull"); |
| 340 | + }); |
| 341 | + |
| 342 | + it("should warn and return z.any() for unsupported schema types", () => { |
| 343 | + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); |
| 344 | + |
| 345 | + const jsonSchema = { type: "custom_unsupported" }; |
| 346 | + const result = convertJsonSchemaToZodSchema(jsonSchema, true); |
| 347 | + |
| 348 | + expect(result._def.typeName).toBe("ZodAny"); |
| 349 | + expect(warnSpy).toHaveBeenCalledWith( |
| 350 | + expect.stringContaining( |
| 351 | + 'Unsupported JSON schema type "custom_unsupported"', |
| 352 | + ), |
| 353 | + ); |
| 354 | + |
| 355 | + warnSpy.mockRestore(); |
| 356 | + }); |
194 | 357 | }); |
195 | 358 |
|
196 | 359 | describe("jsonSchemaToActionParameters", () => { |
|
0 commit comments