Polymorphic Deserialization

WIP

The Problem

My team was faced with an interesting challange where the shape of an object changed in a JSON collection. The collection only consisted of a few items but the shape was not consistent which broke the deserialization.

The example below consists of a JSON payload with 2 items, the common properties are Id, ProductType, ShortDescription. Each then have a unique property of Foo and Bar respectively.

1
2
3
4
5
6
7
8
9
10
11
[{
"Id": "511d1dda-3c62-47e1-9197-c84564f39520",
"ProductType": "XML 2 SMS",
"ShortDescription": "Designed for developers, the XML / HTTP interface will allow you to send and receive SMS messages and query current and historical logs using HTTP request and XML response.",
"Foo": "Some foo property"
}, {
"Id": "1f2bd60f-8fe8-4e55-aebd-fa50e2b51086",
"ProductType": "SMPP 2 SMS",
"ShortDescription": "SMPP (Short Message Peer to Peer) is an industry standard form of connection for SMS Messaging.",
"Bar": "Some bar property"
}]

Solution (God Object)

Typically this could be deserialized to an object that has all of the properties mashed together and anything that doesnt exist in the payload will get the default value (normally null)

1
var myDeserializedClass = JsonConvert.DeserializeObject<SmsProduct>(myJsonResponse);

The SmsProduct entity would then look like this

1
2
3
4
5
6
7
8
public class SmsProduct
{
public string Id { get; set; }
public string ProductType { get; set; }
public string ShortDescription { get; set; }
public string Foo { get; set; }
public string Bar { get; set; }
}

Solution (Custom converter)

This is more complex and if you are a API provider this may cause your consumers headache. The flow of the custom serilizer at a high level would be:

  1. Identify a Discriminator, this is something common to all the entities where discrimination can be applied. For the JSON example above this would be ProductType as we have two options that should repeat over the collection:
1
2
"ProductType": "XML 2 SMS"
"ProductType": "SMPP 2 SMS"

This is just an enum

1
2
3
4
5
enum ProductTypeDiscriminator
{
XML2SMS,
SMPP2SMS
}
  1. The microsoft example then does the some validations on the Utf8JsonReader reader, I have substituted with my example values and between the property checks the reader needs to be advanced with reader.Read();
1
2
3
4
5
6
7
8
9
10
11
12
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();

if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();

string propertyName = reader.GetString();
if (propertyName != "ProductType")
throw new JsonException();

if (reader.TokenType != JsonTokenType.Number)
throw new JsonException();
  1. Extract the discriminator from the reader and create a response object based on it.
1
2
3
4
5
6
7
var discriminator = (ProductTypeDiscriminator)reader.GetInt32();
Person person = typeDiscriminator switch
{
TypeDiscriminator.Customer => new Customer(),
TypeDiscriminator.Employee => new Employee(),
_ => throw new JsonException()
};
  1. Itterate over the reader and set each property value manually.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return person;
}

if (reader.TokenType == JsonTokenType.PropertyName)
{
propertyName = reader.GetString();
reader.Read();
switch (propertyName)
{
case "CreditLimit":
decimal creditLimit = reader.GetDecimal();
((Customer)person).CreditLimit = creditLimit;
break;
case "OfficeNumber":
string officeNumber = reader.GetString();
((Employee)person).OfficeNumber = officeNumber;
break;
case "Name":
string name = reader.GetString();
person.Name = name;
break;
}
}
}

An alternative could be to read the json object before progressing the reader and then deserializing it.

  • needs a copy of the reader as copiedReader (its a struct so will be a copy on the heap)
  • the reader needs to be completed else it will shout, a hack is to have while (reader.Read()) and then a condition to check for if (reader.TokenType == JsonTokenType.EndObject)
1
2
var jsonObject = JsonDocument.ParseValue(ref copiedReader).RootElement.GetRawText();
var myDto = (MyDto) JsonSerializer.Deserialize(jsonObject, myDtoType, DeserializerOptions.Default);

Register

1
2
3
4
5
var serializeOptions = new JsonSerializerOptions();

serializeOptions
.Converters
.Add(new SmsProductConverterWithTypeDiscriminator());

References