Skip to content

Commit ff99cba

Browse files
SystemTextSerializer: Fixes serialization type in ToStream method (#5517)
## Description Use `typeof(T)` instead of `input.GetType()` to preserve polymorphic serialization. When T is a base type with `[JsonPolymorphic]`, this ensures the `$type` discriminator is included. Using `input.GetType()` would serialize as the concrete type, bypassing polymorphic attributes.. ## Type of change Please delete options that are not relevant. - [X] Bug fix (non-breaking change which fixes an issue) --------- Co-authored-by: Kiran Kumar Kolli <[email protected]>
1 parent 895c8d5 commit ff99cba

File tree

7 files changed

+327
-99
lines changed

7 files changed

+327
-99
lines changed

Microsoft.Azure.Cosmos.Samples/Usage/SystemTextJson/CosmosSystemTextJsonSerializer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public override T FromStream<T>(Stream stream)
4747
public override Stream ToStream<T>(T input)
4848
{
4949
MemoryStream streamPayload = new MemoryStream();
50-
this.systemTextJsonSerializer.Serialize(streamPayload, input, input.GetType(), default);
50+
this.systemTextJsonSerializer.Serialize(streamPayload, input, typeof(T), default);
5151
streamPayload.Position = 0;
5252
return streamPayload;
5353
}

Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTestsCommon.cs

Lines changed: 59 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ namespace Microsoft.Azure.Cosmos.Services.Management.Tests
2525
using Microsoft.Azure.Documents;
2626
using Microsoft.VisualStudio.TestTools.UnitTesting;
2727
using Newtonsoft.Json;
28-
using Newtonsoft.Json.Linq;
29-
28+
using Newtonsoft.Json.Linq;
29+
3030
internal class LinqTestsCommon
3131
{
3232
/// <summary>
@@ -36,18 +36,18 @@ internal class LinqTestsCommon
3636
/// <param name="dataResults"></param>
3737
/// <returns></returns>
3838
private static bool CompareListOfAnonymousType(List<object> queryResults, List<dynamic> dataResults, bool ignoreOrder)
39-
{
40-
if (!ignoreOrder)
41-
{
42-
return queryResults.SequenceEqual(dataResults);
43-
}
44-
45-
if (queryResults.Count != dataResults.Count)
46-
{
47-
return false;
48-
}
49-
50-
bool resultMatched = true;
39+
{
40+
if (!ignoreOrder)
41+
{
42+
return queryResults.SequenceEqual(dataResults);
43+
}
44+
45+
if (queryResults.Count != dataResults.Count)
46+
{
47+
return false;
48+
}
49+
50+
bool resultMatched = true;
5151
foreach (object obj in queryResults)
5252
{
5353
if (!dataResults.Any(a => a.Equals(obj)))
@@ -64,8 +64,8 @@ private static bool CompareListOfAnonymousType(List<object> queryResults, List<d
6464
resultMatched = false;
6565
break;
6666
}
67-
}
68-
67+
}
68+
6969
return resultMatched;
7070
}
7171

@@ -533,15 +533,15 @@ Family createDataObj(Random random)
533533
return getQuery;
534534
}
535535

536-
public static Func<bool, IQueryable<Data>> GenerateSimpleCosmosData(Cosmos.Database cosmosDatabase, bool useRandomData = true)
536+
public static Func<bool, IQueryable<Data>> GenerateSimpleCosmosData(Cosmos.Database cosmosDatabase, bool useRandomData = true)
537537
{
538538
const int DocumentCount = 10;
539539
PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition { Paths = new System.Collections.ObjectModel.Collection<string>(new[] { "/Pk" }), Kind = PartitionKind.Hash };
540540
Container container = cosmosDatabase.CreateContainerAsync(new ContainerProperties { Id = Guid.NewGuid().ToString(), PartitionKey = partitionKeyDefinition }).Result;
541541

542-
ILinqTestDataGenerator dataGenerator = useRandomData ? new LinqTestRandomDataGenerator(DocumentCount) : new LinqTestDataGenerator(DocumentCount);
543-
List<Data> testData = new List<Data>(dataGenerator.GenerateData());
544-
foreach (Data dataEntry in testData)
542+
ILinqTestDataGenerator dataGenerator = useRandomData ? new LinqTestRandomDataGenerator(DocumentCount) : new LinqTestDataGenerator(DocumentCount);
543+
List<Data> testData = new List<Data>(dataGenerator.GenerateData());
544+
foreach (Data dataEntry in testData)
545545
{
546546
Data response = container.CreateItemAsync<Data>(dataEntry, new Cosmos.PartitionKey(dataEntry.Pk)).Result;
547547
}
@@ -593,32 +593,32 @@ public static LinqTestOutput ExecuteTest(LinqTestInput input, bool serializeResu
593593
}
594594

595595
public static string BuildExceptionMessageForTest(Exception ex)
596-
{
597-
StringBuilder message = new StringBuilder();
596+
{
597+
StringBuilder message = new StringBuilder();
598598
do
599599
{
600600
if (ex is CosmosException cosmosException)
601-
{
602-
message.Append($"Status Code: {cosmosException.StatusCode}");
601+
{
602+
message.Append($"Status Code: {cosmosException.StatusCode}");
603603
}
604604
else if (ex is DocumentClientException documentClientException)
605605
{
606606
message.Append(documentClientException.RawErrorMessage);
607607
}
608608
else
609-
{
610-
message.Append(ex.Message);
609+
{
610+
message.Append(ex.Message);
611611
}
612-
612+
613613
ex = ex.InnerException;
614614
if (ex != null)
615615
{
616616
message.Append(",");
617617
}
618618
}
619-
while (ex != null);
620-
621-
return message.ToString();
619+
while (ex != null);
620+
621+
return message.ToString();
622622
}
623623
}
624624

@@ -675,27 +675,27 @@ public class LinqTestInput : BaselineTestInput
675675
// - unordered query since the results are not deterministics for LinQ results and actual query results
676676
// - scenarios not supported in LINQ, e.g. sequence doesn't contain element.
677677
internal bool skipVerification;
678-
679-
// Ignore Ordering for AnonymousType object
680-
internal bool ignoreOrder;
681-
682-
internal bool serializeOutput;
678+
679+
// Ignore Ordering for AnonymousType object
680+
internal bool ignoreOrder;
681+
682+
internal bool serializeOutput;
683683

684684
internal LinqTestInput(
685685
string description,
686686
Expression<Func<bool, IQueryable>> expr,
687-
bool skipVerification = false,
687+
bool skipVerification = false,
688688
bool ignoreOrder = false,
689689
string expressionStr = null,
690-
string inputData = null,
690+
string inputData = null,
691691
bool serializeOutput = false)
692692
: base(description)
693693
{
694694
this.Expression = expr ?? throw new ArgumentNullException($"{nameof(expr)} must not be null.");
695-
this.skipVerification = skipVerification;
695+
this.skipVerification = skipVerification;
696696
this.ignoreOrder = ignoreOrder;
697697
this.expressionStr = expressionStr;
698-
this.inputData = inputData;
698+
this.inputData = inputData;
699699
this.serializeOutput = serializeOutput;
700700
}
701701

@@ -761,7 +761,7 @@ public class LinqTestOutput : BaselineTestOutput
761761
{ "WHERE", "\nWHERE" },
762762
{ "JOIN", "\nJOIN" },
763763
{ "ORDER BY", "\nORDER BY" },
764-
{ "OFFSET", "\nOFFSET" },
764+
{ "OFFSET", "\nOFFSET" },
765765
{ "GROUP BY", "\nGROUP BY" },
766766
{ " )", "\n)" }
767767
};
@@ -852,14 +852,14 @@ public override void SerializeAsXml(XmlWriter xmlWriter)
852852
}
853853
}
854854

855-
class SystemTextJsonLinqSerializer : CosmosLinqSerializer
855+
internal class SystemTextJsonLinqSerializer : CosmosLinqSerializer
856856
{
857-
private readonly JsonObjectSerializer systemTextJsonSerializer;
857+
private readonly JsonObjectSerializer systemTextJsonSerializer;
858858
private readonly JsonSerializerOptions jsonSerializerOptions;
859859

860860
public SystemTextJsonLinqSerializer(JsonSerializerOptions jsonSerializerOptions)
861861
{
862-
this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions);
862+
this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions);
863863
this.jsonSerializerOptions = jsonSerializerOptions;
864864
}
865865

@@ -894,66 +894,27 @@ public override Stream ToStream<T>(T input)
894894

895895
public override string SerializeMemberName(MemberInfo memberInfo)
896896
{
897-
System.Text.Json.Serialization.JsonExtensionDataAttribute jsonExtensionDataAttribute =
898-
memberInfo.GetCustomAttribute<System.Text.Json.Serialization.JsonExtensionDataAttribute>(true);
899-
if (jsonExtensionDataAttribute != null)
900-
{
901-
return null;
902-
}
903-
904-
JsonPropertyNameAttribute jsonPropertyNameAttribute = memberInfo.GetCustomAttribute<JsonPropertyNameAttribute>(true);
905-
if (!string.IsNullOrEmpty(jsonPropertyNameAttribute?.Name))
906-
{
907-
return jsonPropertyNameAttribute.Name;
908-
}
909-
910-
if (this.jsonSerializerOptions.PropertyNamingPolicy != null)
911-
{
912-
return this.jsonSerializerOptions.PropertyNamingPolicy.ConvertName(memberInfo.Name);
913-
}
914-
915-
// Do any additional handling of JsonSerializerOptions here.
916-
917-
return memberInfo.Name;
918-
}
919-
}
920-
921-
class SystemTextJsonSerializer : CosmosSerializer
922-
{
923-
private readonly JsonObjectSerializer systemTextJsonSerializer;
924-
925-
public SystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions)
926-
{
927-
this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions);
928-
}
929-
930-
public override T FromStream<T>(Stream stream)
931-
{
932-
if (stream == null)
933-
throw new ArgumentNullException(nameof(stream));
934-
935-
using (stream)
897+
System.Text.Json.Serialization.JsonExtensionDataAttribute jsonExtensionDataAttribute =
898+
memberInfo.GetCustomAttribute<System.Text.Json.Serialization.JsonExtensionDataAttribute>(true);
899+
if (jsonExtensionDataAttribute != null)
936900
{
937-
if (stream.CanSeek && stream.Length == 0)
938-
{
939-
return default;
940-
}
901+
return null;
902+
}
941903

942-
if (typeof(Stream).IsAssignableFrom(typeof(T)))
943-
{
944-
return (T)(object)stream;
945-
}
904+
JsonPropertyNameAttribute jsonPropertyNameAttribute = memberInfo.GetCustomAttribute<JsonPropertyNameAttribute>(true);
905+
if (!string.IsNullOrEmpty(jsonPropertyNameAttribute?.Name))
906+
{
907+
return jsonPropertyNameAttribute.Name;
908+
}
946909

947-
return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default);
910+
if (this.jsonSerializerOptions.PropertyNamingPolicy != null)
911+
{
912+
return this.jsonSerializerOptions.PropertyNamingPolicy.ConvertName(memberInfo.Name);
948913
}
949-
}
950914

951-
public override Stream ToStream<T>(T input)
952-
{
953-
MemoryStream streamPayload = new MemoryStream();
954-
this.systemTextJsonSerializer.Serialize(streamPayload, input, input.GetType(), default);
955-
streamPayload.Position = 0;
956-
return streamPayload;
915+
// Do any additional handling of JsonSerializerOptions here.
916+
917+
return memberInfo.Name;
957918
}
958919
}
959920
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="SystemTextJsonSerializer.cs" company="Microsoft Corporation">
3+
// Copyright (c) Microsoft Corporation. All rights reserved.
4+
// </copyright>
5+
//-----------------------------------------------------------------------
6+
namespace Microsoft.Azure.Cosmos.Services.Management.Tests
7+
{
8+
using System;
9+
using System.IO;
10+
using System.Text.Json;
11+
using global::Azure.Core.Serialization;
12+
13+
internal class SystemTextJsonSerializer : CosmosSerializer
14+
{
15+
private readonly JsonObjectSerializer systemTextJsonSerializer;
16+
17+
public SystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions)
18+
{
19+
this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions);
20+
}
21+
22+
public override T FromStream<T>(Stream stream)
23+
{
24+
if (stream == null)
25+
throw new ArgumentNullException(nameof(stream));
26+
27+
using (stream)
28+
{
29+
if (stream.CanSeek && stream.Length == 0)
30+
{
31+
return default;
32+
}
33+
34+
if (typeof(Stream).IsAssignableFrom(typeof(T)))
35+
{
36+
return (T)(object)stream;
37+
}
38+
39+
return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default);
40+
}
41+
}
42+
43+
public override Stream ToStream<T>(T input)
44+
{
45+
MemoryStream streamPayload = new MemoryStream();
46+
this.systemTextJsonSerializer.Serialize(streamPayload, input, typeof(T), default);
47+
streamPayload.Position = 0;
48+
return streamPayload;
49+
}
50+
}
51+
}

Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Json/CosmosSystemTextJsonSerializerTest.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using System.Linq;
77
using System.Reflection;
8+
using System.Text.Json;
89
using Microsoft.Azure.Cosmos.Tests.Poco.STJ;
910
using Microsoft.VisualStudio.TestTools.UnitTesting;
1011

@@ -176,5 +177,54 @@ public void TestSerializeMemberName()
176177
Assert.AreEqual(member.Name, this.stjSerializer.SerializeMemberName(member));
177178
}
178179
}
180+
181+
[TestMethod]
182+
public void TestPolymorphicSerialization_IncludesTypeDiscriminator()
183+
{
184+
// Arrange.
185+
Shape circle = new Circle
186+
{
187+
Id = "circle",
188+
Color = "Red",
189+
Radius = 5.0
190+
};
191+
192+
// Act.
193+
Stream serializedStream = this.stjSerializer.ToStream(circle);
194+
using StreamReader reader = new(serializedStream);
195+
string json = reader.ReadToEnd();
196+
197+
// Assert.
198+
using JsonDocument jsonDocument = JsonDocument.Parse(json);
199+
JsonElement rootElement = jsonDocument.RootElement;
200+
201+
Assert.AreEqual("Circle", rootElement.GetProperty("$type").GetString());
202+
Assert.AreEqual(5.0, rootElement.GetProperty("radius").GetDouble());
203+
}
204+
205+
[TestMethod]
206+
public void TestPolymorphicSerialization_SerializeDeserialize_PreservesType()
207+
{
208+
// Arrange.
209+
Shape original = new Circle
210+
{
211+
Id = "circle",
212+
Color = "Green",
213+
Radius = 7.5
214+
};
215+
216+
// Act.
217+
Stream serializedStream = this.stjSerializer.ToStream(original);
218+
Shape deserialized = this.stjSerializer.FromStream<Shape>(serializedStream);
219+
220+
// Assert.
221+
Assert.IsNotNull(deserialized);
222+
Assert.IsInstanceOfType(deserialized, typeof(Circle));
223+
224+
Circle deserializedCircle = (Circle)deserialized;
225+
Assert.AreEqual(original.Id, deserializedCircle.Id);
226+
Assert.AreEqual(original.Color, deserializedCircle.Color);
227+
Assert.AreEqual(((Circle)original).Radius, deserializedCircle.Radius);
228+
}
179229
}
180230
}

0 commit comments

Comments
 (0)