Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is polymorphic deserialization possible in System.Text.Json?

I try to migrate from Newtonsoft.Json to System.Text.Json. I want to deserialize abstract class. Newtonsoft.Json has TypeNameHandling for this. Is there any way to deserialize abstract class via System.Text.Json on .net core 3.0?

like image 931
SkyStorm Avatar asked Sep 24 '19 06:09

SkyStorm


People also ask

What is polymorphic JSON?

You can use this schema when defining XML Type hierarchies by using only the base XML Types. The XML schema defines XML Types that inherit from each other. In the JSON, an object carries no additional information about the type.

Which is better Newtonsoft JSON or System text JSON?

Json does case-insensitive property name matching by default. The System. Text. Json default is case-sensitive, which gives better performance since it's doing an exact match.

What is JObject alternative in System text JSON?

The four new types are JsonArray , JsonObject , JsonNode and JsonValue . The closest type to JObject is JsonObject which offers similar functionality.

What is System JSON deserialize?

Deserializes the specified JSON string into an Apex object of the specified type. deserializeUntyped(jsonString) Deserializes the specified JSON string into collections of primitive data types. serialize(objectToSerialize) Serializes Apex objects into JSON content.


2 Answers

Is polymorphic deserialization possible in System.Text.Json?

The answer is yes and no, depending on what you mean by "possible".

There is no polymorphic deserialization (equivalent to Newtonsoft.Json's TypeNameHandling) support built-in to System.Text.Json. This is because reading the .NET type name specified as a string within the JSON payload (such as $type metadata property) to create your objects is not recommended since it introduces potential security concerns (see https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 for more info).

Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.

However, there is a way to add your own support for polymorphic deserialization by creating a JsonConverter<T>, so in that sense, it is possible.

The docs show an example of how to do that using a type discriminator property: https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization

Let's look at an example.

Say you have a base class and a couple of derived classes:

public class BaseClass {     public int Int { get; set; } } public class DerivedA : BaseClass {     public string Str { get; set; } } public class DerivedB : BaseClass {     public bool Bool { get; set; } } 

You can create the following JsonConverter<BaseClass> that writes the type discriminator while serializing and reads it to figure out which type to deserialize. You can register that converter on the JsonSerializerOptions.

public class BaseClassConverter : JsonConverter<BaseClass> {     private enum TypeDiscriminator     {         BaseClass = 0,         DerivedA = 1,         DerivedB = 2     }      public override bool CanConvert(Type type)     {         return typeof(BaseClass).IsAssignableFrom(type);     }      public override BaseClass Read(         ref Utf8JsonReader reader,         Type typeToConvert,         JsonSerializerOptions options)     {         if (reader.TokenType != JsonTokenType.StartObject)         {             throw new JsonException();         }          if (!reader.Read()                 || reader.TokenType != JsonTokenType.PropertyName                 || reader.GetString() != "TypeDiscriminator")         {             throw new JsonException();         }          if (!reader.Read() || reader.TokenType != JsonTokenType.Number)         {             throw new JsonException();         }          BaseClass baseClass;         TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();         switch (typeDiscriminator)         {             case TypeDiscriminator.DerivedA:                 if (!reader.Read() || reader.GetString() != "TypeValue")                 {                     throw new JsonException();                 }                 if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)                 {                     throw new JsonException();                 }                 baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));                 break;             case TypeDiscriminator.DerivedB:                 if (!reader.Read() || reader.GetString() != "TypeValue")                 {                     throw new JsonException();                 }                 if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)                 {                     throw new JsonException();                 }                 baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));                 break;             default:                 throw new NotSupportedException();         }          if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)         {             throw new JsonException();         }          return baseClass;     }      public override void Write(         Utf8JsonWriter writer,         BaseClass value,         JsonSerializerOptions options)     {         writer.WriteStartObject();          if (value is DerivedA derivedA)         {             writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);             writer.WritePropertyName("TypeValue");             JsonSerializer.Serialize(writer, derivedA);         }         else if (value is DerivedB derivedB)         {             writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);             writer.WritePropertyName("TypeValue");             JsonSerializer.Serialize(writer, derivedB);         }         else         {             throw new NotSupportedException();         }          writer.WriteEndObject();     } } 

This is what serialization and deserialization would look like (including comparison with Newtonsoft.Json):

private static void PolymorphicSupportComparison() {     var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };      // Using: System.Text.Json     var options = new JsonSerializerOptions     {         Converters = { new BaseClassConverter() },         WriteIndented = true     };      string jsonString = JsonSerializer.Serialize(objects, options);     Console.WriteLine(jsonString);     /*      [       {         "TypeDiscriminator": 1,         "TypeValue": {             "Str": null,             "Int": 0         }       },       {         "TypeDiscriminator": 2,         "TypeValue": {             "Bool": false,             "Int": 0         }       }      ]     */      var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);       // Using: Newtonsoft.Json     var settings = new Newtonsoft.Json.JsonSerializerSettings     {         TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,         Formatting = Newtonsoft.Json.Formatting.Indented     };      jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);     Console.WriteLine(jsonString);     /*      [       {         "$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",         "Str": null,         "Int": 0       },       {         "$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",         "Bool": false,         "Int": 0       }      ]     */      var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);      Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType()); } 

Here's another StackOverflow question that shows how to support polymorphic deserialization with interfaces (rather than abstract classes), but a similar solution would apply for any polymorphism: Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?

like image 129
ahsonkhan Avatar answered Sep 22 '22 18:09

ahsonkhan


I ended up with that solution. It's lightwight and a generic enougth for me.

The type discriminator converter

public class TypeDiscriminatorConverter<T> : JsonConverter<T> where T : ITypeDiscriminator {     private readonly IEnumerable<Type> _types;      public TypeDiscriminatorConverter()     {         var type = typeof(T);         _types = AppDomain.CurrentDomain.GetAssemblies()             .SelectMany(s => s.GetTypes())             .Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)             .ToList();     }      public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)     {         if (reader.TokenType != JsonTokenType.StartObject)         {             throw new JsonException();         }          using (var jsonDocument = JsonDocument.ParseValue(ref reader))         {             if (!jsonDocument.RootElement.TryGetProperty(nameof(ITypeDiscriminator.TypeDiscriminator), out var typeProperty))             {                 throw new JsonException();             }              var type = _types.FirstOrDefault(x => x.Name == typeProperty.GetString());             if (type == null)             {                 throw new JsonException();             }              var jsonObject = jsonDocument.RootElement.GetRawText();             var result = (T) JsonSerializer.Deserialize(jsonObject, type, options);              return result;         }     }      public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)     {         JsonSerializer.Serialize(writer, (object)value, options);     } } 

The interface

public interface ITypeDiscriminator {     string TypeDiscriminator { get; } } 

And the example models

public interface ISurveyStepResult : ITypeDiscriminator {     string Id { get; set; } }  public class BoolStepResult : ISurveyStepResult {     public string Id { get; set; }     public string TypeDiscriminator => nameof(BoolStepResult);      public bool Value { get; set; } }  public class TextStepResult : ISurveyStepResult {     public string Id { get; set; }     public string TypeDiscriminator => nameof(TextStepResult);      public string Value { get; set; } }  public class StarsStepResult : ISurveyStepResult {     public string Id { get; set; }     public string TypeDiscriminator => nameof(StarsStepResult);      public int Value { get; set; } } 

And here is the test method

public void SerializeAndDeserializeTest()     {         var surveyResult = new SurveyResultModel()         {             Id = "id",             SurveyId = "surveyId",             Steps = new List<ISurveyStepResult>()             {                 new BoolStepResult(){ Id = "1", Value = true},                 new TextStepResult(){ Id = "2", Value = "some text"},                 new StarsStepResult(){ Id = "3", Value = 5},             }         };          var jsonSerializerOptions = new JsonSerializerOptions()         {             Converters = { new TypeDiscriminatorConverter<ISurveyStepResult>()},             WriteIndented = true         };         var result = JsonSerializer.Serialize(surveyResult, jsonSerializerOptions);          var back = JsonSerializer.Deserialize<SurveyResultModel>(result, jsonSerializerOptions);          var result2 = JsonSerializer.Serialize(back, jsonSerializerOptions);                  Assert.IsTrue(back.Steps.Count == 3                        && back.Steps.Any(x => x is BoolStepResult)                       && back.Steps.Any(x => x is TextStepResult)                       && back.Steps.Any(x => x is StarsStepResult)                       );         Assert.AreEqual(result2, result);     } 
like image 35
Demetrius Axenowski Avatar answered Sep 22 '22 18:09

Demetrius Axenowski