@@ -279,6 +279,92 @@ describe("convertJsonSchemaToZodSchema", () => {
279279 warnSpy . mockRestore ( ) ;
280280 } ) ;
281281
282+ it ( "should resolve the same $ref used by multiple sibling properties" , ( ) => {
283+ // Two properties reference the same $def — the visited set must NOT
284+ // mark the second usage as circular.
285+ const jsonSchema = {
286+ type : "object" ,
287+ properties : {
288+ billing : { $ref : "#/$defs/Address" } ,
289+ shipping : { $ref : "#/$defs/Address" } ,
290+ } ,
291+ required : [ "billing" , "shipping" ] ,
292+ $defs : {
293+ Address : {
294+ type : "object" ,
295+ properties : {
296+ street : { type : "string" , description : "Street" } ,
297+ city : { type : "string" , description : "City" } ,
298+ } ,
299+ required : [ "street" , "city" ] ,
300+ description : "An address" ,
301+ } ,
302+ } ,
303+ } ;
304+
305+ const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } ) ;
306+
307+ const result = convertJsonSchemaToZodSchema ( jsonSchema , true ) ;
308+ const shape = ( result as z . ZodObject < any > ) . shape ;
309+
310+ // Both should be fully resolved objects, NOT z.any()
311+ expect ( shape . billing . _def . typeName ) . toBe ( "ZodObject" ) ;
312+ expect ( shape . shipping . _def . typeName ) . toBe ( "ZodObject" ) ;
313+
314+ // No circular-ref warning should have been emitted
315+ expect ( warnSpy ) . not . toHaveBeenCalled ( ) ;
316+
317+ warnSpy . mockRestore ( ) ;
318+ } ) ;
319+
320+ it ( "should resolve a shared $ref used in different branches of a $ref chain" , ( ) => {
321+ // Wrapper -> Container (via $ref) which has two children both using $ref to Leaf.
322+ // Without set cloning, the second Leaf ref in Container would be wrongly flagged
323+ // as circular because the first Leaf resolution already added it to the set.
324+ const jsonSchema = {
325+ type : "object" ,
326+ properties : {
327+ wrapper : { $ref : "#/$defs/Container" } ,
328+ } ,
329+ required : [ "wrapper" ] ,
330+ $defs : {
331+ Container : {
332+ type : "object" ,
333+ properties : {
334+ first : { $ref : "#/$defs/Leaf" } ,
335+ second : { $ref : "#/$defs/Leaf" } ,
336+ } ,
337+ required : [ "first" , "second" ] ,
338+ description : "A container with two leaves" ,
339+ } ,
340+ Leaf : {
341+ type : "object" ,
342+ properties : {
343+ label : { type : "string" , description : "Leaf label" } ,
344+ } ,
345+ required : [ "label" ] ,
346+ description : "A leaf node" ,
347+ } ,
348+ } ,
349+ } ;
350+
351+ const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } ) ;
352+
353+ const result = convertJsonSchemaToZodSchema ( jsonSchema , true ) ;
354+ const wrapperShape = (
355+ ( result as z . ZodObject < any > ) . shape . wrapper as z . ZodObject < any >
356+ ) . shape ;
357+
358+ // Both first and second should be fully resolved Leaf objects
359+ expect ( wrapperShape . first . _def . typeName ) . toBe ( "ZodObject" ) ;
360+ expect ( wrapperShape . second . _def . typeName ) . toBe ( "ZodObject" ) ;
361+
362+ // No circular-ref warning should have been emitted
363+ expect ( warnSpy ) . not . toHaveBeenCalled ( ) ;
364+
365+ warnSpy . mockRestore ( ) ;
366+ } ) ;
367+
282368 it ( "should handle anyOf with $ref variants" , ( ) => {
283369 const jsonSchema = {
284370 type : "object" ,
0 commit comments