@@ -41,6 +41,9 @@ function bundle<S extends object = JSONSchema, O extends ParserOptions<S> = Pars
4141
4242 // Remap all $ref pointers
4343 remap < S , O > ( inventory , options ) ;
44+
45+ // Fix any $ref paths that traverse through other $refs (which is invalid per JSON Schema spec)
46+ fixRefsThroughRefs ( inventory , parser . schema as any ) ;
4447}
4548
4649/**
@@ -316,4 +319,94 @@ function removeFromInventory(inventory: InventoryEntry[], entry: any) {
316319 const index = inventory . indexOf ( entry ) ;
317320 inventory . splice ( index , 1 ) ;
318321}
322+
323+ /**
324+ * After remapping, some $ref paths may traverse through other $ref nodes.
325+ * JSON pointer resolution does not follow $ref indirection, so these paths are invalid.
326+ * This function detects and fixes such paths by following any intermediate $refs
327+ * to compute a valid direct path.
328+ */
329+ function fixRefsThroughRefs ( inventory : InventoryEntry [ ] , schema : any ) {
330+ for ( const entry of inventory ) {
331+ if ( ! entry . $ref || typeof entry . $ref !== "object" || ! ( "$ref" in entry . $ref ) ) {
332+ continue ;
333+ }
334+
335+ const refValue = entry . $ref . $ref ;
336+ if ( typeof refValue !== "string" || ! refValue . startsWith ( "#/" ) ) {
337+ continue ;
338+ }
339+
340+ const fixedPath = resolvePathThroughRefs ( schema , refValue ) ;
341+ if ( fixedPath !== refValue ) {
342+ entry . $ref . $ref = fixedPath ;
343+ }
344+ }
345+ }
346+
347+ /**
348+ * Walks a JSON pointer path through the schema. If any intermediate value
349+ * is a $ref, follows it and adjusts the path accordingly.
350+ * Returns the corrected path that doesn't traverse through any $ref.
351+ */
352+ function resolvePathThroughRefs ( schema : any , refPath : string ) : string {
353+ if ( ! refPath . startsWith ( "#/" ) ) {
354+ return refPath ;
355+ }
356+
357+ const segments = refPath . slice ( 2 ) . split ( "/" ) ;
358+ let current = schema ;
359+ const resolvedSegments : string [ ] = [ ] ;
360+
361+ for ( const seg of segments ) {
362+ if ( current === null || current === undefined || typeof current !== "object" ) {
363+ // Can't walk further, return original path
364+ return refPath ;
365+ }
366+
367+ // If the current value is a $ref, follow it
368+ if ( "$ref" in current && typeof current . $ref === "string" && current . $ref . startsWith ( "#/" ) ) {
369+ // Follow the $ref and restart the path from its target
370+ const targetSegments = current . $ref . slice ( 2 ) . split ( "/" ) ;
371+ resolvedSegments . length = 0 ;
372+ resolvedSegments . push ( ...targetSegments ) ;
373+ current = walkPath ( schema , current . $ref ) ;
374+ if ( current === null || current === undefined || typeof current !== "object" ) {
375+ return refPath ;
376+ }
377+ }
378+
379+ const decoded = seg . replace ( / ~ 1 / g, "/" ) . replace ( / ~ 0 / g, "~" ) ;
380+ const idx = Array . isArray ( current ) ? parseInt ( decoded ) : decoded ;
381+ current = current [ idx ] ;
382+ resolvedSegments . push ( seg ) ;
383+ }
384+
385+ const result = "#/" + resolvedSegments . join ( "/" ) ;
386+ return result ;
387+ }
388+
389+ /**
390+ * Walks a JSON pointer path through a schema object, returning the value at that path.
391+ */
392+ function walkPath ( schema : any , path : string ) : any {
393+ if ( ! path . startsWith ( "#/" ) ) {
394+ return undefined ;
395+ }
396+
397+ const segments = path . slice ( 2 ) . split ( "/" ) ;
398+ let current = schema ;
399+
400+ for ( const seg of segments ) {
401+ if ( current === null || current === undefined || typeof current !== "object" ) {
402+ return undefined ;
403+ }
404+ const decoded = seg . replace ( / ~ 1 / g, "/" ) . replace ( / ~ 0 / g, "~" ) ;
405+ const idx = Array . isArray ( current ) ? parseInt ( decoded ) : decoded ;
406+ current = current [ idx ] ;
407+ }
408+
409+ return current ;
410+ }
411+
319412export default bundle ;
0 commit comments