@@ -473,6 +473,255 @@ public void DeserializeArrayWithAllNullElements_DoesNotHang()
473473 }
474474
475475 #endregion
476+
477+ #region Skip() Nested Mapping Tests
478+
479+ /// <summary>
480+ /// Model with properties that include nested mappings to test Skip() behavior.
481+ /// </summary>
482+ public class ServiceConfig
483+ {
484+ public string ? Name { get ; set ; }
485+ public DeploymentConfig ? Deployment { get ; set ; }
486+ public string ? Namespace { get ; set ; }
487+ }
488+
489+ public class DeploymentConfig
490+ {
491+ public int Replicas { get ; set ; }
492+ public ResourceConfig ? Resources { get ; set ; }
493+ }
494+
495+ public class ResourceConfig
496+ {
497+ public string ? Cpu { get ; set ; }
498+ public string ? Memory { get ; set ; }
499+ }
500+
501+ /// <summary>
502+ /// Model with sibling discriminator for testing unknown discriminator Skip().
503+ /// </summary>
504+ public class ServiceWithPlugins
505+ {
506+ public string ? Name { get ; set ; }
507+ public PluginType PluginKind { get ; set ; }
508+
509+ [ YamlSiblingDiscriminator ( nameof ( PluginKind ) ) ]
510+ [ YamlDiscriminatorMapping ( nameof ( PluginType . Metrics ) , typeof ( MetricsPlugin ) ) ]
511+ [ YamlDiscriminatorMapping ( nameof ( PluginType . Logging ) , typeof ( LoggingPlugin ) ) ]
512+ public PluginBase ? Plugin { get ; set ; }
513+
514+ public string ? Description { get ; set ; }
515+ }
516+
517+ public enum PluginType
518+ {
519+ Metrics ,
520+ Logging ,
521+ Tracing
522+ }
523+
524+ public abstract class PluginBase
525+ {
526+ }
527+
528+ public class MetricsPlugin : PluginBase
529+ {
530+ public int Port { get ; set ; }
531+ }
532+
533+ public class LoggingPlugin : PluginBase
534+ {
535+ public string ? Level { get ; set ; }
536+ }
537+
538+ /// <summary>
539+ /// Test: Skip() on a nested mapping should advance past the nested MappingEnd,
540+ /// not the parent's MappingEnd. This was the root cause of the OOM bug.
541+ /// When deserializing a mapping and encountering an unknown property with a nested
542+ /// mapping value, Skip() must consume the entire nested mapping AND advance past
543+ /// its MappingEnd marker, so the parent loop continues correctly.
544+ /// </summary>
545+ [ Fact ]
546+ public void Deserialize_UnknownPropertyWithNestedMapping_PropertiesAfterStillParsed ( )
547+ {
548+ var yaml = """
549+ name: my-service
550+ unknown-nested-property:
551+ nested-key1: value1
552+ nested-key2: value2
553+ deeply-nested:
554+ key: value
555+ namespace: production
556+ """ ;
557+
558+ // The key issue: when unknown-nested-property is skipped, the reader should
559+ // end up AFTER the nested MappingEnd, so "namespace" is still read.
560+ var result = YamlSerializer . Deserialize ( yaml , InfiniteLoopTestContext . Default . ServiceConfig ) ;
561+
562+ Assert . NotNull ( result ) ;
563+ Assert . Equal ( "my-service" , result . Name ) ;
564+ Assert . Equal ( "production" , result . Namespace ) ; // This was not being set before the fix
565+ }
566+
567+ /// <summary>
568+ /// Test: Multiple unknown properties with nested mappings should all be skipped correctly.
569+ /// </summary>
570+ [ Fact ]
571+ public void Deserialize_MultipleUnknownPropertiesWithNestedMappings_DoesNotHang ( )
572+ {
573+ var yaml = """
574+ name: my-service
575+ unknown-property-1:
576+ key1: value1
577+ unknown-property-2:
578+ nested:
579+ deep: value
580+ deployment:
581+ replicas: 3
582+ resources:
583+ cpu: 100m
584+ memory: 256Mi
585+ unknown-property-3:
586+ another: nested
587+ namespace: production
588+ """ ;
589+
590+ var result = YamlSerializer . Deserialize ( yaml , InfiniteLoopTestContext . Default . ServiceConfig ) ;
591+
592+ Assert . NotNull ( result ) ;
593+ Assert . Equal ( "my-service" , result . Name ) ;
594+ Assert . Equal ( "production" , result . Namespace ) ;
595+ Assert . NotNull ( result . Deployment ) ;
596+ Assert . Equal ( 3 , result . Deployment . Replicas ) ;
597+ Assert . NotNull ( result . Deployment . Resources ) ;
598+ Assert . Equal ( "100m" , result . Deployment . Resources . Cpu ) ;
599+ Assert . Equal ( "256Mi" , result . Deployment . Resources . Memory ) ;
600+ }
601+
602+ /// <summary>
603+ /// Test: Unknown property with deeply nested mapping should be skipped correctly.
604+ /// </summary>
605+ [ Fact ]
606+ public void Deserialize_DeeplyNestedUnknownProperty_DoesNotHang ( )
607+ {
608+ var yaml = """
609+ name: my-service
610+ very-deeply-nested-unknown:
611+ level1:
612+ level2:
613+ level3:
614+ level4:
615+ level5:
616+ key: value
617+ namespace: production
618+ """ ;
619+
620+ var result = YamlSerializer . Deserialize ( yaml , InfiniteLoopTestContext . Default . ServiceConfig ) ;
621+
622+ Assert . NotNull ( result ) ;
623+ Assert . Equal ( "my-service" , result . Name ) ;
624+ Assert . Equal ( "production" , result . Namespace ) ;
625+ }
626+
627+ /// <summary>
628+ /// Test: Sibling discriminator with unknown discriminator value should skip the property
629+ /// and continue parsing remaining properties without causing infinite loop.
630+ /// </summary>
631+ [ Fact ]
632+ public void Deserialize_SiblingDiscriminatorWithUnknownValue_DoesNotHang ( )
633+ {
634+ var yaml = """
635+ name: my-service
636+ plugin-kind: Tracing
637+ plugin:
638+ endpoint: http://jaeger:14268
639+ sample-rate: 0.1
640+ description: A service with unknown plugin type
641+ """ ;
642+
643+ // PluginKind.Tracing exists as enum value but has no mapping to a concrete type
644+ // The generated code should skip the plugin property and continue to description
645+ var result = YamlSerializer . Deserialize ( yaml , InfiniteLoopTestContext . Default . ServiceWithPlugins ) ;
646+
647+ Assert . NotNull ( result ) ;
648+ Assert . Equal ( "my-service" , result . Name ) ;
649+ Assert . Equal ( PluginType . Tracing , result . PluginKind ) ;
650+ Assert . Null ( result . Plugin ) ; // Unknown discriminator, plugin not deserialized
651+ Assert . Equal ( "A service with unknown plugin type" , result . Description ) ; // Should still be parsed
652+ }
653+
654+ /// <summary>
655+ /// Test: Sibling discriminator with known value works correctly.
656+ /// </summary>
657+ [ Fact ]
658+ public void Deserialize_SiblingDiscriminatorWithKnownValue_WorksCorrectly ( )
659+ {
660+ var yaml = """
661+ name: metrics-service
662+ plugin-kind: Metrics
663+ plugin:
664+ port: 9090
665+ description: A service with metrics plugin
666+ """ ;
667+
668+ var result = YamlSerializer . Deserialize ( yaml , InfiniteLoopTestContext . Default . ServiceWithPlugins ) ;
669+
670+ Assert . NotNull ( result ) ;
671+ Assert . Equal ( "metrics-service" , result . Name ) ;
672+ Assert . Equal ( PluginType . Metrics , result . PluginKind ) ;
673+ Assert . NotNull ( result . Plugin ) ;
674+ Assert . IsType < MetricsPlugin > ( result . Plugin ) ;
675+ Assert . Equal ( 9090 , ( ( MetricsPlugin ) result . Plugin ) . Port ) ;
676+ Assert . Equal ( "A service with metrics plugin" , result . Description ) ;
677+ }
678+
679+ /// <summary>
680+ /// Test: List of items with sibling discriminator where some have unknown values.
681+ /// </summary>
682+ [ Fact ]
683+ public void Deserialize_ListWithSiblingDiscriminatorSomeUnknown_DoesNotHang ( )
684+ {
685+ var yaml = """
686+ - name: service-1
687+ plugin-kind: Metrics
688+ plugin:
689+ port: 9090
690+ description: Has metrics
691+ - name: service-2
692+ plugin-kind: Tracing
693+ plugin:
694+ endpoint: http://jaeger
695+ description: Has unknown tracing
696+ - name: service-3
697+ plugin-kind: Logging
698+ plugin:
699+ level: debug
700+ description: Has logging
701+ """ ;
702+
703+ var result = YamlSerializer . Deserialize ( yaml , InfiniteLoopTestContext . Default . ListServiceWithPlugins ) ;
704+
705+ Assert . NotNull ( result ) ;
706+ Assert . Equal ( 3 , result . Count ) ;
707+
708+ // First: known discriminator
709+ Assert . Equal ( "service-1" , result [ 0 ] . Name ) ;
710+ Assert . IsType < MetricsPlugin > ( result [ 0 ] . Plugin ) ;
711+ Assert . Equal ( "Has metrics" , result [ 0 ] . Description ) ;
712+
713+ // Second: unknown discriminator (Tracing has no mapping)
714+ Assert . Equal ( "service-2" , result [ 1 ] . Name ) ;
715+ Assert . Null ( result [ 1 ] . Plugin ) ;
716+ Assert . Equal ( "Has unknown tracing" , result [ 1 ] . Description ) ;
717+
718+ // Third: known discriminator
719+ Assert . Equal ( "service-3" , result [ 2 ] . Name ) ;
720+ Assert . IsType < LoggingPlugin > ( result [ 2 ] . Plugin ) ;
721+ Assert . Equal ( "Has logging" , result [ 2 ] . Description ) ;
722+ }
723+
724+ #endregion
476725}
477726
478727/// <summary>
@@ -488,6 +737,14 @@ public void DeserializeArrayWithAllNullElements_DoesNotHang()
488737[ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . ContainerWithObjectDictionary ) ) ]
489738[ YamlSerializable ( typeof ( List < InfiniteLoopRegressionTests . ObjectItem > ) ) ]
490739[ YamlSerializable ( typeof ( Dictionary < string , InfiniteLoopRegressionTests . ObjectItem > ) ) ]
740+ [ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . ServiceConfig ) ) ]
741+ [ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . DeploymentConfig ) ) ]
742+ [ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . ResourceConfig ) ) ]
743+ [ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . ServiceWithPlugins ) ) ]
744+ [ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . PluginBase ) ) ]
745+ [ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . MetricsPlugin ) ) ]
746+ [ YamlSerializable ( typeof ( InfiniteLoopRegressionTests . LoggingPlugin ) ) ]
747+ [ YamlSerializable ( typeof ( List < InfiniteLoopRegressionTests . ServiceWithPlugins > ) ) ]
491748public partial class InfiniteLoopTestContext : YamlSerializerContext
492749{
493750}
0 commit comments