55using System . Diagnostics ;
66using System . Diagnostics . CodeAnalysis ;
77using System . Globalization ;
8+ using System . Runtime . InteropServices ;
89using System . Text . Json . Nodes ;
910using System . Text . Json . Serialization ;
1011using System . Text . Json . Serialization . Metadata ;
@@ -76,9 +77,12 @@ private static JsonSchema MapJsonSchemaCore(
7677 {
7778 Debug . Assert ( typeInfo . IsConfigured ) ;
7879
79- if ( cacheResult && state . TryPushType ( typeInfo , propertyInfo , out string ? existingJsonPointer ) )
80+ JsonSchemaExporterContext exporterContext = state . CreateContext ( typeInfo , propertyInfo , parentPolymorphicTypeInfo ) ;
81+
82+ if ( cacheResult && typeInfo . Kind is not JsonTypeInfoKind . None &&
83+ state . TryGetExistingJsonPointer ( exporterContext , out string ? existingJsonPointer ) )
8084 {
81- // We're generating the schema of a recursive type , return a reference pointing to the outermost schema .
85+ // The schema context has already been generated in the schema document , return a reference to it .
8286 return CompleteSchema ( ref state , new JsonSchema { Ref = existingJsonPointer } ) ;
8387 }
8488
@@ -364,17 +368,12 @@ JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema)
364368 {
365369 schema . MakeNullable ( ) ;
366370 }
367-
368- if ( cacheResult )
369- {
370- state . PopGeneratedType ( ) ;
371- }
372371 }
373372
374373 if ( state . ExporterOptions . TransformSchemaNode != null )
375374 {
376375 // Prime the schema for invocation by the JsonNode transformer.
377- schema . ExporterContext = state . CreateContext ( typeInfo , propertyInfo , parentPolymorphicTypeInfo ) ;
376+ schema . ExporterContext = exporterContext ;
378377 }
379378
380379 return schema ;
@@ -409,7 +408,7 @@ private static bool IsPolymorphicTypeThatSpecifiesItselfAsDerivedType(JsonTypeIn
409408 private readonly ref struct GenerationState ( JsonSerializerOptions options , JsonSchemaExporterOptions exporterOptions )
410409 {
411410 private readonly List < string > _currentPath = [ ] ;
412- private readonly List < ( JsonTypeInfo typeInfo , JsonPropertyInfo ? propertyInfo , int depth ) > _generationStack = [ ] ;
411+ private readonly Dictionary < ( JsonTypeInfo , JsonPropertyInfo ? ) , string [ ] > _generated = new ( ) ;
413412
414413 public int CurrentDepth => _currentPath . Count ;
415414 public JsonSerializerOptions Options { get ; } = options ;
@@ -432,77 +431,75 @@ public void PopSchemaNode()
432431 }
433432
434433 /// <summary>
435- /// Pushes the current type/property to the generation stack or returns a JSON pointer if the type is recursive .
434+ /// Registers the current schema node generation context; if it has already been generated return a JSON pointer to its location .
436435 /// </summary>
437- public bool TryPushType ( JsonTypeInfo typeInfo , JsonPropertyInfo ? propertyInfo , [ NotNullWhen ( true ) ] out string ? existingJsonPointer )
436+ public bool TryGetExistingJsonPointer ( in JsonSchemaExporterContext context , [ NotNullWhen ( true ) ] out string ? existingJsonPointer )
438437 {
439- foreach ( ( JsonTypeInfo otherTypeInfo , JsonPropertyInfo ? otherPropertyInfo , int depth ) in _generationStack )
438+ ( JsonTypeInfo TypeInfo , JsonPropertyInfo ? PropertyInfo ) key = ( context . TypeInfo , context . PropertyInfo ) ;
439+ #if NET
440+ ref string [ ] ? pathToSchema = ref CollectionsMarshal . GetValueRefOrAddDefault ( _generated , key , out bool exists ) ;
441+ #else
442+ bool exists = _generated . TryGetValue ( key , out string [ ] ? pathToSchema ) ;
443+ #endif
444+ if ( exists )
440445 {
441- if ( typeInfo == otherTypeInfo && propertyInfo == otherPropertyInfo )
442- {
443- existingJsonPointer = FormatJsonPointer ( _currentPath , depth ) ;
444- return true ;
445- }
446+ existingJsonPointer = FormatJsonPointer ( pathToSchema ) ;
447+ return true ;
446448 }
447-
448- _generationStack . Add ( ( typeInfo , propertyInfo , CurrentDepth ) ) ;
449+ #if NET
450+ pathToSchema = context . _path ;
451+ #else
452+ _generated [ key ] = context . _path ;
453+ #endif
449454 existingJsonPointer = null ;
450455 return false ;
451456 }
452457
453- public void PopGeneratedType ( )
454- {
455- Debug . Assert ( _generationStack . Count > 0 ) ;
456- _generationStack . RemoveAt ( _generationStack . Count - 1 ) ;
457- }
458-
459458 public JsonSchemaExporterContext CreateContext ( JsonTypeInfo typeInfo , JsonPropertyInfo ? propertyInfo , JsonTypeInfo ? baseTypeInfo )
460459 {
461- return new JsonSchemaExporterContext ( typeInfo , propertyInfo , baseTypeInfo , _currentPath . ToArray ( ) ) ;
460+ return new JsonSchemaExporterContext ( typeInfo , propertyInfo , baseTypeInfo , [ .. _currentPath ] ) ;
462461 }
463462
464- private static string FormatJsonPointer ( List < string > currentPathList , int depth )
463+ private static string FormatJsonPointer ( ReadOnlySpan < string > path )
465464 {
466- Debug . Assert ( 0 <= depth && depth < currentPathList . Count ) ;
467-
468- if ( depth == 0 )
465+ if ( path . IsEmpty )
469466 {
470467 return "#" ;
471468 }
472469
473- using ValueStringBuilder sb = new ( initialCapacity : depth * 10 ) ;
470+ using ValueStringBuilder sb = new ( initialCapacity : path . Length * 10 ) ;
474471 sb . Append ( '#' ) ;
475472
476- for ( int i = 0 ; i < depth ; i ++ )
473+ foreach ( string segment in path )
477474 {
478- ReadOnlySpan < char > segment = currentPathList [ i ] . AsSpan ( ) ;
475+ ReadOnlySpan < char > span = segment . AsSpan ( ) ;
479476 sb . Append ( '/' ) ;
480477
481478 do
482479 {
483480 // Per RFC 6901 the characters '~' and '/' must be escaped.
484- int pos = segment . IndexOfAny ( '~' , '/' ) ;
481+ int pos = span . IndexOfAny ( '~' , '/' ) ;
485482 if ( pos < 0 )
486483 {
487- sb . Append ( segment ) ;
484+ sb . Append ( span ) ;
488485 break ;
489486 }
490487
491- sb . Append ( segment . Slice ( 0 , pos ) ) ;
488+ sb . Append ( span . Slice ( 0 , pos ) ) ;
492489
493- if ( segment [ pos ] == '~' )
490+ if ( span [ pos ] == '~' )
494491 {
495492 sb . Append ( "~0" ) ;
496493 }
497494 else
498495 {
499- Debug . Assert ( segment [ pos ] == '/' ) ;
496+ Debug . Assert ( span [ pos ] == '/' ) ;
500497 sb . Append ( "~1" ) ;
501498 }
502499
503- segment = segment . Slice ( pos + 1 ) ;
500+ span = span . Slice ( pos + 1 ) ;
504501 }
505- while ( ! segment . IsEmpty ) ;
502+ while ( ! span . IsEmpty ) ;
506503 }
507504
508505 return sb . ToString ( ) ;
0 commit comments