@@ -81,9 +81,9 @@ public void RoundTripTuple(JsonSerializer addSerializers, JsonSerializer expecte
8181
8282 [ Theory ]
8383 [ InlineData ( JsonSerializer . None , JsonSerializer . FieldEnabled ) ]
84- [ InlineData ( JsonSerializer . CustomGlobal , JsonSerializer . FieldEnabled ) ]
85- [ InlineData ( JsonSerializer . CustomPerType , JsonSerializer . FieldEnabled ) ]
86- [ InlineData ( JsonSerializer . CustomPerType | JsonSerializer . CustomGlobal , JsonSerializer . FieldEnabled ) ]
84+ [ InlineData ( JsonSerializer . CustomGlobal , JsonSerializer . CustomGlobal ) ]
85+ [ InlineData ( JsonSerializer . CustomPerType , JsonSerializer . CustomPerType ) ]
86+ [ InlineData ( JsonSerializer . CustomPerType | JsonSerializer . CustomGlobal , JsonSerializer . CustomPerType ) ]
8787 public void RoundTripValueTuple ( JsonSerializer addSerializers , JsonSerializer expectedSerializer )
8888 {
8989 var obj = RoundTrip ( ( 42 , "abc" ) , """{"Item1":42,"Item2":"abc"}"""u8 , expectedSerializer , addSerializers ) ;
@@ -93,16 +93,146 @@ public void RoundTripValueTuple(JsonSerializer addSerializers, JsonSerializer ex
9393
9494 [ Theory ]
9595 [ InlineData ( JsonSerializer . None , JsonSerializer . FieldEnabled ) ]
96- [ InlineData ( JsonSerializer . CustomGlobal , JsonSerializer . FieldEnabled ) ]
97- [ InlineData ( JsonSerializer . CustomPerType , JsonSerializer . FieldEnabled ) ]
98- [ InlineData ( JsonSerializer . CustomPerType | JsonSerializer . CustomGlobal , JsonSerializer . FieldEnabled ) ]
96+ [ InlineData ( JsonSerializer . CustomGlobal , JsonSerializer . CustomGlobal ) ]
97+ [ InlineData ( JsonSerializer . CustomPerType , JsonSerializer . CustomPerType ) ]
98+ [ InlineData ( JsonSerializer . CustomPerType | JsonSerializer . CustomGlobal , JsonSerializer . CustomPerType ) ]
9999 public void RoundTripNamedValueTuple ( JsonSerializer addSerializers , JsonSerializer expectedSerializer )
100100 {
101101 var obj = RoundTrip ( ( X : 42 , Y : "abc" ) , """{"Item1":42,"Item2":"abc"}"""u8 , expectedSerializer , addSerializers ) ;
102102 Assert . Equal ( 42 , obj . X ) ;
103103 Assert . Equal ( "abc" , obj . Y ) ;
104104 }
105105
106+ [ Fact ]
107+ public void RoundTripValueTupleList ( )
108+ {
109+ List < ( int , string ) > source = [ ( 1 , "a" ) , ( 2 , "b" ) ] ;
110+ var clone = RoundTrip ( source , """[{"Item1":1,"Item2":"a"},{"Item1":2,"Item2":"b"}]"""u8 , JsonSerializer . FieldEnabled ) ;
111+ Assert . Equal ( source , clone ) ;
112+ }
113+
114+ [ Fact ]
115+ public void RoundTripValueTupleArray ( )
116+ {
117+ ( int , string ) [ ] source = [ ( 1 , "a" ) , ( 2 , "b" ) ] ;
118+ var clone = RoundTrip ( source , """[{"Item1":1,"Item2":"a"},{"Item1":2,"Item2":"b"}]"""u8 , JsonSerializer . FieldEnabled ) ;
119+ Assert . Equal ( source , clone ) ;
120+ }
121+
122+ [ Fact ]
123+ public void RoundTripTupleList ( )
124+ {
125+ List < Tuple < int , string > > source = [ Tuple . Create ( 1 , "a" ) , Tuple . Create ( 2 , "b" ) ] ;
126+ var clone = RoundTrip ( source , """[{"Item1":1,"Item2":"a"},{"Item1":2,"Item2":"b"}]"""u8 , JsonSerializer . Default ) ;
127+ Assert . Equal ( source , clone ) ;
128+ }
129+
130+ [ Fact ]
131+ public void RoundTripTupleArray ( )
132+ {
133+ Tuple < int , string > [ ] source = [ Tuple . Create ( 1 , "a" ) , Tuple . Create ( 2 , "b" ) ] ;
134+ var clone = RoundTrip ( source , """[{"Item1":1,"Item2":"a"},{"Item1":2,"Item2":"b"}]"""u8 , JsonSerializer . Default ) ;
135+ Assert . Equal ( source , clone ) ;
136+ }
137+
138+ [ Fact ]
139+ public void RoundTripFieldOnlyPoco ( )
140+ {
141+ var source = new FieldOnlyPoco { X = 1 , Y = "a" } ;
142+ var clone = RoundTrip ( source , """{"X":1,"Y":"a"}"""u8 , JsonSerializer . FieldEnabled ) ;
143+ Assert . Equal ( 1 , clone . X ) ;
144+ Assert . Equal ( "a" , clone . Y ) ;
145+ }
146+
147+ [ Fact ]
148+ public void RoundTripPropertyOnlyPoco ( )
149+ {
150+ var source = new PropertyOnlyPoco { X = 1 , Y = "a" } ;
151+ var clone = RoundTrip ( source , """{"X":1,"Y":"a"}"""u8 , JsonSerializer . Default ) ;
152+ Assert . Equal ( 1 , clone . X ) ;
153+ Assert . Equal ( "a" , clone . Y ) ;
154+ }
155+
156+ [ Fact ]
157+ public void RoundTripMixedPoco ( )
158+ {
159+ // this is the self-inflicted scenario; intent isn't obvious, so: we defer to STJ conventions,
160+ // which means we lose the field
161+ var source = new MixedFieldPropertyPoco { X = 1 , Y = "a" } ;
162+ var clone = RoundTrip ( source , """{"Y":"a"}"""u8 , JsonSerializer . Default ) ;
163+ Assert . Equal ( 0 , clone . X ) ; // <== drop
164+ Assert . Equal ( "a" , clone . Y ) ;
165+ }
166+
167+ [ Fact ]
168+ public void RoundTripTree ( )
169+ {
170+ NodeA < string > source = new NodeA < string >
171+ {
172+ Value = "abc" ,
173+ Next = new ( )
174+ {
175+ Value = "def"
176+ }
177+ } ;
178+
179+ var clone = RoundTrip ( source , """{"Next":{"Next":null,"Value":"def"},"Value":"abc"}"""u8 , JsonSerializer . Default ) ;
180+ Assert . Equal ( "abc" , clone . Value ) ;
181+ Assert . NotNull ( clone . Next ) ;
182+ Assert . Equal ( "def" , clone . Next . Value ) ;
183+ Assert . Null ( clone . Next . Next ) ;
184+ }
185+
186+ [ Fact ]
187+ public void RoundTripDictionary ( )
188+ {
189+ Dictionary < string , ( int id , string who , DateTime when ) > source = new ( )
190+ {
191+ [ "x" ] = ( 42 , "Fred" , new ( 2025 , 03 , 18 ) ) ,
192+ [ "y" ] = ( 43 , "Barney" , new ( 2025 , 03 , 22 ) ) ,
193+ } ;
194+ var clone = RoundTrip ( source ,
195+ """{"x":{"Item1":42,"Item2":"Fred","Item3":"2025-03-18T00:00:00"},"y":{"Item1":43,"Item2":"Barney","Item3":"2025-03-22T00:00:00"}}"""u8 ,
196+ JsonSerializer . FieldEnabled ) ;
197+ Assert . Equal ( 2 , clone . Count ) ;
198+ Assert . True ( clone . TryGetValue ( "x" , out var val ) ) ;
199+ Assert . Equal ( source [ "x" ] , val ) ;
200+ Assert . True ( clone . TryGetValue ( "y" , out val ) ) ;
201+ Assert . Equal ( source [ "y" ] , val ) ;
202+ }
203+
204+ public class FieldOnlyPoco
205+ {
206+ public int X ;
207+ public string ? Y ;
208+ }
209+
210+ public class PropertyOnlyPoco
211+ {
212+ public int X { get ; set ; }
213+ public string ? Y { get ; set ; }
214+ }
215+
216+ public class MixedFieldPropertyPoco
217+ {
218+ public int X ; // field
219+ public string ? Y { get ; set ; } // property
220+ }
221+
222+ public class NodeA < T >
223+ {
224+ public NodeB < T > ? Next { get ; set ; }
225+
226+ public T ? Value { get ; set ; }
227+ }
228+
229+ public class NodeB < T >
230+ {
231+ public NodeA < T > ? Next { get ; set ; }
232+
233+ public T ? Value { get ; set ; }
234+ }
235+
106236 private static T RoundTrip < T > ( T value , ReadOnlySpan < byte > expectedBytes , JsonSerializer expectedJsonOptions , JsonSerializer addSerializers = JsonSerializer . None , bool binary = false )
107237 {
108238 var services = new ServiceCollection ( ) ;
@@ -112,13 +242,13 @@ private static T RoundTrip<T>(T value, ReadOnlySpan<byte> expectedBytes, JsonSer
112242
113243 if ( ( addSerializers & JsonSerializer . CustomGlobal ) != JsonSerializer . None )
114244 {
115- globalOptions = new ( ) ;
245+ globalOptions = new ( ) { IncludeFields = true } ; // assume any custom options will serialize the whole type
116246 services . AddKeyedSingleton < JsonSerializerOptions > ( typeof ( IHybridCacheSerializer < > ) , globalOptions ) ;
117247 }
118248
119249 if ( ( addSerializers & JsonSerializer . CustomPerType ) != JsonSerializer . None )
120250 {
121- perTypeOptions = new ( ) ;
251+ perTypeOptions = new ( ) { IncludeFields = true } ; // assume any custom options will serialize the whole type
122252 services . AddKeyedSingleton < JsonSerializerOptions > ( typeof ( IHybridCacheSerializer < T > ) , perTypeOptions ) ;
123253 }
124254
0 commit comments