@@ -786,5 +786,253 @@ public void DeserializeFalseSchemaParsesAsNotEmptySchema()
786786 Assert . Empty ( schema . Not . AllOf ?? [ ] ) ;
787787 Assert . Empty ( schema . Not . OneOf ?? [ ] ) ;
788788 }
789+
790+ [ Fact ]
791+ public async Task ParseSchemaReferencePreservesJsonSchema2020KeywordSiblings ( )
792+ {
793+ // Arrange
794+ var yaml = """
795+ openapi: 3.2.0
796+ info:
797+ title: Sibling preservation repro
798+ version: 1.0.0
799+ paths: {}
800+ components:
801+ schemas:
802+ Target:
803+ type: object
804+ properties:
805+ name:
806+ type: string
807+ Referencing:
808+ $ref: '#/components/schemas/Target'
809+ description: Sibling description
810+ $dynamicAnchor: anchor
811+ $defs:
812+ sibling:
813+ $dynamicAnchor: inner
814+ $ref: '#/components/schemas/Target'
815+ """ ;
816+
817+ // Act
818+ using var stream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( yaml ) ) ;
819+ var result = await OpenApiDocument . LoadAsync ( stream , "yaml" , SettingsFixture . ReaderSettings ) ;
820+ var referencing = result . Document . Components ! . Schemas [ "Referencing" ] ;
821+
822+ // Assert — siblings are preserved on the OpenApiSchemaReference
823+ referencing . Should ( ) . BeOfType < OpenApiSchemaReference > ( ) ;
824+ referencing . Description . Should ( ) . Be ( "Sibling description" ) ;
825+ referencing . DynamicAnchor . Should ( ) . Be ( "anchor" ) ;
826+ referencing . Definitions . Should ( ) . NotBeNull ( ) ;
827+ referencing . Definitions ! . Should ( ) . ContainKey ( "sibling" ) ;
828+ referencing . Definitions [ "sibling" ] . DynamicAnchor . Should ( ) . Be ( "inner" ) ;
829+ }
830+
831+ [ Fact ]
832+ public async Task SerializeSchemaReferencePreservesJsonSchema2020KeywordSiblings ( )
833+ {
834+ // Arrange
835+ var yaml = """
836+ openapi: 3.2.0
837+ info:
838+ title: Sibling preservation repro
839+ version: 1.0.0
840+ paths: {}
841+ components:
842+ schemas:
843+ Target:
844+ type: object
845+ properties:
846+ name:
847+ type: string
848+ Referencing:
849+ $ref: '#/components/schemas/Target'
850+ $dynamicAnchor: anchor
851+ $defs:
852+ itemType:
853+ $dynamicAnchor: itemType
854+ $ref: '#/components/schemas/Target'
855+ """ ;
856+
857+ // Act — parse then serialize back
858+ using var stream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( yaml ) ) ;
859+ var result = await OpenApiDocument . LoadAsync ( stream , "yaml" , SettingsFixture . ReaderSettings ) ;
860+ var writer = new StringWriter ( ) ;
861+ result . Document . SerializeAsV32 ( new OpenApiYamlWriter ( writer ) ) ;
862+ var output = writer . ToString ( ) ;
863+
864+ // Assert — round-trip preserves $dynamicAnchor and $defs alongside $ref
865+ using var roundTripStream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( output ) ) ;
866+ var roundTripResult = await OpenApiDocument . LoadAsync ( roundTripStream , "yaml" , SettingsFixture . ReaderSettings ) ;
867+ var referencing = roundTripResult . Document . Components ! . Schemas [ "Referencing" ] ;
868+
869+ referencing . Should ( ) . BeOfType < OpenApiSchemaReference > ( ) ;
870+ referencing . DynamicAnchor . Should ( ) . Be ( "anchor" ) ;
871+ referencing . Definitions . Should ( ) . NotBeNull ( ) ;
872+ referencing . Definitions ! . Should ( ) . ContainKey ( "itemType" ) ;
873+ referencing . Definitions [ "itemType" ] . DynamicAnchor . Should ( ) . Be ( "itemType" ) ;
874+ }
875+
876+ [ Fact ]
877+ public async Task ParseSchemaReferencePreservesScalarKeywordSiblings ( )
878+ {
879+ // Arrange
880+ var yaml = """
881+ openapi: 3.2.0
882+ info:
883+ title: Scalar sibling repro
884+ version: 1.0.0
885+ paths: {}
886+ components:
887+ schemas:
888+ Target:
889+ type: object
890+ Referencing:
891+ $ref: '#/components/schemas/Target'
892+ $id: 'https://example.com/referencing.json'
893+ $schema: 'https://json-schema.org/draft/2020-12/schema'
894+ $comment: A comment sibling
895+ $anchor: myAnchor
896+ $dynamicRef: '#myAnchor'
897+ """ ;
898+
899+ // Act
900+ using var stream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( yaml ) ) ;
901+ var result = await OpenApiDocument . LoadAsync ( stream , "yaml" , SettingsFixture . ReaderSettings ) ;
902+ var referencing = result . Document . Components ! . Schemas [ "Referencing" ] ;
903+
904+ // Assert
905+ referencing . Should ( ) . BeOfType < OpenApiSchemaReference > ( ) ;
906+ referencing . Id . Should ( ) . Be ( "https://example.com/referencing.json" ) ;
907+ referencing . Schema . Should ( ) . Be ( new Uri ( "https://json-schema.org/draft/2020-12/schema" ) ) ;
908+ referencing . Comment . Should ( ) . Be ( "A comment sibling" ) ;
909+ ( ( IOpenApiSchemaMissingProperties ) referencing ) . Anchor . Should ( ) . Be ( "myAnchor" ) ;
910+ referencing . DynamicRef . Should ( ) . Be ( "#myAnchor" ) ;
911+ }
912+
913+ [ Fact ]
914+ public async Task SerializeSchemaReferencePreservesScalarKeywordSiblings ( )
915+ {
916+ // Arrange
917+ var yaml = """
918+ openapi: 3.2.0
919+ info:
920+ title: Scalar round-trip
921+ version: 1.0.0
922+ paths: {}
923+ components:
924+ schemas:
925+ Target:
926+ type: object
927+ Referencing:
928+ $ref: '#/components/schemas/Target'
929+ $id: 'https://example.com/referencing.json'
930+ $schema: 'https://json-schema.org/draft/2020-12/schema'
931+ $comment: A comment sibling
932+ $anchor: myAnchor
933+ $dynamicRef: '#myAnchor'
934+ """ ;
935+
936+ // Act — parse then serialize back
937+ using var stream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( yaml ) ) ;
938+ var result = await OpenApiDocument . LoadAsync ( stream , "yaml" , SettingsFixture . ReaderSettings ) ;
939+ var writer = new StringWriter ( ) ;
940+ result . Document . SerializeAsV32 ( new OpenApiYamlWriter ( writer ) ) ;
941+ var output = writer . ToString ( ) ;
942+
943+ // Assert — round-trip preserves scalar siblings alongside $ref
944+ using var roundTripStream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( output ) ) ;
945+ var roundTripResult = await OpenApiDocument . LoadAsync ( roundTripStream , "yaml" , SettingsFixture . ReaderSettings ) ;
946+ var referencing = roundTripResult . Document . Components ! . Schemas [ "Referencing" ] ;
947+
948+ referencing . Should ( ) . BeOfType < OpenApiSchemaReference > ( ) ;
949+ referencing . Id . Should ( ) . Be ( "https://example.com/referencing.json" ) ;
950+ referencing . Schema . Should ( ) . Be ( new Uri ( "https://json-schema.org/draft/2020-12/schema" ) ) ;
951+ referencing . Comment . Should ( ) . Be ( "A comment sibling" ) ;
952+ ( ( IOpenApiSchemaMissingProperties ) referencing ) . Anchor . Should ( ) . Be ( "myAnchor" ) ;
953+ referencing . DynamicRef . Should ( ) . Be ( "#myAnchor" ) ;
954+ }
955+
956+ [ Fact ]
957+ public async Task ParseSchemaReferencePreservesVocabularySibling ( )
958+ {
959+ // Arrange
960+ var yaml = """
961+ openapi: 3.2.0
962+ info:
963+ title: Vocabulary sibling repro
964+ version: 1.0.0
965+ paths: {}
966+ components:
967+ schemas:
968+ Target:
969+ type: object
970+ Referencing:
971+ $ref: '#/components/schemas/Target'
972+ $vocabulary:
973+ 'https://json-schema.org/draft/2020-12/vocab/core': true
974+ 'https://json-schema.org/draft/2020-12/vocab/applicator': false
975+ """ ;
976+
977+ // Act
978+ using var stream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( yaml ) ) ;
979+ var result = await OpenApiDocument . LoadAsync ( stream , "yaml" , SettingsFixture . ReaderSettings ) ;
980+ var referencing = result . Document . Components ! . Schemas [ "Referencing" ] ;
981+
982+ // Assert
983+ referencing . Should ( ) . BeOfType < OpenApiSchemaReference > ( ) ;
984+ referencing . Vocabulary . Should ( ) . NotBeNull ( ) ;
985+ referencing . Vocabulary ! . Should ( ) . HaveCount ( 2 ) ;
986+ referencing . Vocabulary [ "https://json-schema.org/draft/2020-12/vocab/core" ] . Should ( ) . BeTrue ( ) ;
987+ referencing . Vocabulary [ "https://json-schema.org/draft/2020-12/vocab/applicator" ] . Should ( ) . BeFalse ( ) ;
988+ }
989+
990+ [ Fact ]
991+ public async Task ParseSchemaReferencePreservesDynamicAnchorInsideDefsInAllOf ( )
992+ {
993+ // Arrange — the allOf-based binding variant: $defs sits inside allOf[0],
994+ // and the nested schema has $ref + $dynamicAnchor (the binding entry).
995+ var yaml = """
996+ openapi: 3.2.0
997+ info:
998+ title: allOf binding variant
999+ version: 1.0.0
1000+ paths: {}
1001+ components:
1002+ schemas:
1003+ Asset:
1004+ type: object
1005+ properties:
1006+ id:
1007+ type: string
1008+ Paged:
1009+ type: object
1010+ properties:
1011+ items:
1012+ type: array
1013+ AssetPaged:
1014+ allOf:
1015+ - $defs:
1016+ contentType:
1017+ $dynamicAnchor: contentType
1018+ $ref: '#/components/schemas/Asset'
1019+ - $ref: '#/components/schemas/Paged'
1020+ """ ;
1021+
1022+ // Act
1023+ using var stream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( yaml ) ) ;
1024+ var result = await OpenApiDocument . LoadAsync ( stream , "yaml" , SettingsFixture . ReaderSettings ) ;
1025+ var assetPaged = result . Document . Components ! . Schemas [ "AssetPaged" ] ;
1026+
1027+ // Assert
1028+ assetPaged . AllOf . Should ( ) . NotBeNull ( ) ;
1029+ assetPaged . AllOf ! . Count . Should ( ) . Be ( 2 ) ;
1030+ var defsHolder = assetPaged . AllOf [ 0 ] ;
1031+ defsHolder . Definitions . Should ( ) . NotBeNull ( ) ;
1032+ defsHolder . Definitions ! . Should ( ) . ContainKey ( "contentType" ) ;
1033+ var contentType = defsHolder . Definitions [ "contentType" ] ;
1034+ contentType . Should ( ) . BeOfType < OpenApiSchemaReference > ( ) ;
1035+ contentType . DynamicAnchor . Should ( ) . Be ( "contentType" ) ;
1036+ }
7891037 }
7901038}
0 commit comments