NEW Try Zapier integration to connect Dasha instantly to thousands of the most popular apps!
Type discriminator converter - TypeScript to C#
Aleksander Alexashin, Engineer
9 minutes
At Dasha we use communication between applications written in weakly-typed and strong-typed languages. For example, TypeScript and C#. Sometimes we need to pass typed messages from TS to C#. This is usually a problem.
A Type discriminator converter is a converter which can be used to serialize/deserialize JSON/xml to a typed Object.
For example, we can describe a union typed interface with a complex indicator(discriminator) field in ts. When we pass an implementation of this message to a C# application we need to convert it, for example, to JSON string and deserialize to strongly typed hierarchy of class in C#.
We searched for an existing library to solve this task. We found some libraries, for example, JsonKnownTypes and JsonSubTypes. We did not look at the Json.NET because it does not provide a custom indicator(discriminator) field. It is not possible to create more than one indicator(discriminator) field in these libraries.
For this task, we created a converter named TypeIndicatorConverter. Not so long ago we used only one library for serialization named Newtonsoft.Json, but in some new components, we start using System.Text.Json. Because these serializers have different implementation and we created an abstraction under these converters and named it TypeIndicatorConverter.Core. We've separated all of the serializer-related information from the main logic by creating a helper abstraction.
Our solution makes it possible to select types while deserializing by more than one field. In addition, the indicator(discriminator) fields can be not a basic type but for example custom objects.
Other advantages our solution is respecting serializer settings such as case-sensitive and specific serializer attributes. Such as [JsonPropertyName] for System.Text.Json and [JsonProperty] and [DataMember] for Newtonsoft.Json.
Example of usage
Imagine we need to write a calculator. By design, calculations can be resource-dependent and very complex. We wrote a UI application to write some expressions. Then we need to send the user-written expression to the server and evaluate it. We also need to get a textual representation of the written expression. By design, we chose to communicate with the server using JSON.
Let's start to develop from a very basic example and do it more complex.
We have four operation: Addition, Multiplication, Division and Subtraction. Also we have constants as operand.
Describe this operation in C# classes.
Introduce abstract class overall expression BaseExpression and say then base abstraction for the indicator(discriminator) converter. Only because before .net 6 System.Text.Json disallows use attribute [JsonConverter] on interface we use an abstract class.
If you use Newtonsoft.Json then you can use interface implementation.
[JsonConverter(typeof(TypeIndicatorConverter<BaseExpression>))] // say use this interface as base for deserializepublicabstractclassBaseExpression{publicabstractdoubleEval();}
Now implement classes responsible for operations and constant value:
Now, if we try to pass an ordinary constant, we will get an error during deserialization, because we have ambiguously determined which type to choose. There are two ways to solve this problem. Allow an ambiguous conclusion, and then the one will be deserialized into the type that has more satisfying indicator fields. Another way, which will be more correct, is to add an indicator field to ordinary constants without changing the already working structure.
Permission of an ambiguous choice of type is done with the help of an attribute [AmbiguousMatching(true)] for base interface:
In some situations, you cannot do without it. But in this situation, we can.
To do this, we indicate in the Value field that it is discriminatory, but its values can be any. To do this, we will use the attribute [TypeIndicator] argument comparingOptions. Specifically, we use ComparingOptions.UnknownValue.
We can rewrite the PiConst class as follows: If we have a ConstName field with a value of PI and not necessarily have a Type field with a value of Const.
Attribute argument ComparingOptions.AllowNotExist allows fields to not exist in JSON representation. But we allowed that when serializing back, the field could have a null value. You can disable this action using the standard attributes for the serializer used.
For Newtonsoft.Json can be used next attribute [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] or for System.Text.Json - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)].
In server-side code we have a helper class for logging expressions. It is a descendant of BaseExpression and we want to exclude this class from deserialization. We can do this by attribute IgnoreIndicators.
For example:
Let's say we updated the UI and it began to send a new type of expressions, but we didn't have time to add them. In our current structure, we will get an error when deserializing. It so happened that such expressions turned out to be optional. In this case, we can create a separate class which will be responsible for such events. This can be done by adding the [FallbackIndicator] attribute to the class.
In these tests we have 4 descendants with one indicator field.
Method
Mean
Error
StdDev
'Direct type deserialization 1 indicators field 4 descendants'
523.3 ns
4.05 ns
5.68 ns
'JsonKnownTypes 1 indicators field 4 descendants'
1,876.4 ns
39.04 ns
57.23 ns
'TypeIndicatorConverter 1 indicators field 4 descendants'
3,369.2 ns
32.69 ns
46.88 ns
'JsonSubTypesConverter 1 indicators field 4 descendants'
11,538.7 ns
41.90 ns
60.08 ns
All the compared serializers use inside Newtonsoft.Json. That means that mean time can not be less than in it. Our implementation is not the fastest but provides more features as a discriminator converter. Naturally, there are places that can be changed and our converter will work faster.
The project is open-source and you can contribute to it.
Source code
The source code of the package and code for the examples used in this article can be found in this repository.