JSON handling is often the bread and butter of an application, as data transit in and out of our project. Unfortunately, we don't always get to choose how they are structured and this can often come with a few challenges when we have to deal with polymorphism.
It may happen that we receive data that comes in different shapes, with some properties overlapping. To tackle this problem, there are a few ways to approach it with more or less type safety.
In this article, we will consider a practical approach, from the most simple, to the most robust.
Our Custom Map of French Hidden Gems
Congrats on your new project! You've just been tasked with building the back-end for a new interactive map of iconic french locations. We will have to ingest data from an external source and pass it on our application.
Unfortunately, the external API is not well structured, and these locations come as a mixed collection of points, with both landmarks and shops in the same array:
[
{
"Name": "Bouillon Chartier",
"Category": "Restaurant",
"Latitude": 48.8759,
"Longitude": 2.3587,
"Description": "Simple french dishes in an authentic 1950s atmosphere.",
"OpeningHours": "11:30 - 00:00",
"HasParking": false
},
{
"Name": "Mont Saint-Michel",
"Period": "Gothic",
"Latitude": 48.6361,
"Longitude": -1.5115,
"Description": "Medieval abbey and UNESCO World Heritage Site."
},
{
"Name": "Grand Café Foy",
"Category": "Café",
"Latitude": 48.6930,
"Longitude": 6.1827,
"Description": "A french café at the heart of the Place Stanislas.",
"OpeningHours": "07:30 - 02:00",
"HasParking": false
},
{
"Name": "Cathedral of Our Lady of Strasbourg",
"Period": "Gothic",
"Latitude": 48.5819,
"Longitude": 7.7513,
"Description": "Catholic cathedral among the finest examples of Rayonnant Gothic architecture."
}
]
What was looking like a simple task might prove to be a little harder than anticipated as every entry has some basic geographic coordinates, but different properties based on their type. To make it even worse, there is no clear discriminator such as Type: "Landmark" for instance.
Fortunately, that's no big deal for the .NET devs we are!
The God Object
Deserializing data might seem straightforward at first glance. If a property might not be present, we can simply tell C# that it might be null. This approach, often called a "God Object", crams all possible properties into a single class, regardless of whether they apply to every instance.
From this statement, we can infer the following Location record:
public record Location(
string Name,
double Latitude,
double Longitude,
string Description,
// Shop properties
ShopCategory? Category,
string? OpeningHours,
bool? HasParking,
// Landmark properties
HistoricalPeriod? Period,
decimal? EntryFee
);
Digesting the data is now child's play:
var json = "...";
var locations = JsonSerializer.Deserialize<List<Location>>(json);
While this will indeed work, it has one major drawback which is that we never know what kind of location we are currently working with. This will result in plenty of null checks, or if through the code.
Moreover, we are clearly seeing that there are two groups here:
We could later parse our location and map each entry to a typed object, but that does not sounds to be very efficient.
Surely .NET has a better way to handle this kind of situation.
Polymorphic Deserialization
What if instead of creating our model (our Location record) around what the deserializer can handle, we proceed the other way around and define it as our data is structured?
Shaping Our Domain
We just saw that we have two kind of data, with a single common ground, let's represent things that way.
First, we represent our shared properties:
public abstract record MapLocation(
string Name,
double Latitude,
double Longitude,
string Description
);
Then, the first type, let's say the shop:
public record Shop(
string Name,
double Latitude,
double Longitude,
string Description,
ShopCategory Category,
string OpeningHours,
bool HasParking
) : MapLocation(Name, Latitude, Longitude, Description);
And finally, the second type we are dealing with, the landmarks:
public record Landmark(
string Name,
double Latitude,
double Longitude,
string Description,
HistoricalPeriod HistoricalPeriod,
decimal? EntryFee
) : MapLocation(Name, Latitude, Longitude, Description)
{
public bool IsFreeEntry => EntryFee is null;
}
Notice that we can already have some intelligence in the typing? A landmark might not have any entry fee and the model can clearly define that. That was not possible in a meaningful way with our god object where every landmark's property could be nullable.
This looks much closer to the domain we are working with, let's test this out!
var locations = JsonSerializer.Deserialize<List<MapLocation>>(json);
Unfortunately, running the previous code results in the following error:
An unhandled exception of type 'System.NotSupportedException' occurred in System.Text.Json.dll: 'Deserialization of interface or abstract types is not supported.
The serializer is not able to infer what kind of data it should deserialize it into, and removing the abstract won't solve the issue as we won't be able to access either Landmark or Shop property.
What we really need is a way to help the serializer identify what kind of data it is dealing with.
Introducing A Converter
System.Text.Json has exactly the kind of type we are looking for: JsonConverter<T>.
A JsonConverter is type that will help our serializer translates a given type to and from JSON, using the Read and Write method. In our example, we only want to help it reading the data, so we will be focusing on the Read part.
Here is the outline of our new converter:
public sealed class MapLocationJsonConverter : JsonConverter<MapLocation>
{
public override MapLocation? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
=> throw new NotImplementedException();
public override void Write(
Utf8JsonWriter writer,
MapLocation value,
JsonSerializerOptions options)
=> throw new NotImplementedException();
}
If our data had an explicit discriminator, such as
$type, we could have used[JsonDerivedType(typeof(...), typeDiscriminator: "...")]and skip this whole part. What makes it tricky here is that the discriminator is implicit by being the presence or the absence of a property.
Focusing on the Read method, we need to find a value in the JSON that will tell us if the current entry is a Shop or a Landmark. There is not a single good answer here, and for the example we will consider that any entry with an HistoricalPeriod property is a Landmark, and a Shop otherwise:
public override MapLocation? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
using var jsonDoc = JsonDocument.ParseValue(ref reader);
var root = jsonDoc.RootElement;
var hasHistoricalPeriod = root.TryGetProperty("Period", out _);
return hasHistoricalPeriod
? root.Deserialize<Landmark>(options)
: root.Deserialize<Shop>(options);
}
Note that
JsonDocumentis used here for simplicity, but for large datasets, consider streaming approaches to avoid memory overhead.
We now have to let the serializer know it's there, and try again:
var options = new JsonSerializerOptions
{
Converters = { new MapLocationJsonConverter() },
};
var locations = JsonSerializer.Deserialize<List<MapLocation>>(json, options);
If we run it again, we can observe that the MapLocation list contains only concrete classes of the appropriate type, hooray!
We can now consume our items with .NET taking care of strongly typing everything.
Wrapping Up
In this article, we tackled the challenge of deserializing polymorphic JSON when no explicit discriminator is available.
We began with a straightforward but somewhat messy approach with a God Object that sacrifices type safety for simplicity. While functional, this method leads to harder debugging and cluttering your code with null checks and reducing maintainability.
We then shifted to a more robust solution using inheritance and a custom JsonConverter. While this requires a bit more upfront effort, it helps ensuring we benefit from the type safety .NET has to offer.
Photo by David Clode on Unsplash


Top comments (0)