In this post, we are going to learn how to create a custom JsonConverter in Json.NET.
Json.NET (aka Newtonsoft.Json) is widely popular for its highly customizable serialization/deserialization mechanism. Many such customizations like property ignoring, null handling, data formatting, etc. are usually achievable by using custom attributes provided by the library. However, there are numerous cases when using attributes is not an option. For example, we may want to keep our domain models clean or we may want conditional output, custom formatting, custom model binding, data validation, etc. We can achieve these by creating a custom JsonConverter
.
Preparation of Data Models
To begin, let’s add some data models.
The Department
enum:
public enum Department { Operations, Admin, CustomerCare }
The Address
record:
public record class Address(string Street, string City);
And finally our primary object model, the Contact
record:
public record class Contact(string Name, Department Department, string Phone, Address Address);
We aim to implement a custom converter for the serialization/deserialization of Contact
.
Simple Way to Create Custom JsonConverter
The simplest way of creating a custom converter is to extend the inbuilt JsonConverter
:
public class ContactConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(Contact); } public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { // TODO } public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } }
For our needs we have to implement three abstract methods:
CanConvert()
allows us to specify certain object types (Contact
in our case) that we intend to convert using this converter.
WriteJson()
holds the desired serialization logic.
ReadJson()
holds the desired deserialization logic.
Serialization and/or Deserialization?
Additionally, we can configure the converter for only serialization or only deserialization roles:
public class ContactConverter : JsonConverter { // omitted for brevity public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override bool CanWrite => true; public override bool CanRead => false; }
By overriding CanWrite
property as true
, we make the converter eligible for serialization. In contrast, the false CanRead
property instructs the framework to ignore this converter in deserialization. That’s why, even though we have to add ReadJson
method (because it is abstract), this will never be called on deserialization.
Custom Serialization by Overriding WriteJson
Now, let’s consider we have a list of contacts:
public class DataSource { public static List<Contact> GetContacts() { return new List<Contact>() { new ("John", Department.Admin, "+1234", new("Street 1","City 1")), new ("Jane", Department.CustomerCare, "+2341", new("Street 2", "City 2")), new ("Mike", Department.Operations, "+3421", new("Street 3", "City 3")) }; } }
And we only want Name
and Department
of contacts in the serialized output. We can define this logic inside WriteJson
method:
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { if (value is not Contact contact) return; writer.WriteStartObject(); writer.WritePropertyName(nameof(contact.Name)); writer.WriteValue(contact.Name); writer.WritePropertyName(nameof(contact.Department)); writer.WriteValue(contact.Department); writer.WriteEndObject(); }
Since our converter only recognizes Contact
object, we cast the value
to a Contact
instance first.
Talking about serialization, we first write the starting node of JSON object ( “{“). Then we write the name of Name
property followed by its value. Similarly, we write the name and value of Department
property. Finally, we wrap the content by writing the end node (“}”).
Using this custom converter is as simple as passing an argument to the usual serialization routine:
var contacts = DataSource.GetContacts(); var json = JsonConvert.SerializeObject(contacts, new ContactConverter());
Like always we call JsonConvert.SerializeObject
which rightly produces the desired output:
[{"Name":"John","Department":1},{"Name":"Jane","Department":2},{"Name":"Mike","Department":0}]
Custom JsonConverter Using Generic
Non-generic JsonConverter
is useful for conditionally playing with different object types. However, to deal with a pre-defined model like ours, we can utilize a more convenient generic converter:
public class ImprovedContactConverter : JsonConverter<Contact> { public override void WriteJson(JsonWriter writer, Contact? contact, JsonSerializer serializer) { // omitted for brevity } public override Contact? ReadJson(JsonReader reader, Type objectType, Contact? existingValue, bool hasExistingValue, JsonSerializer serializer) { throw new NotImplementedException(); } public override bool CanWrite => true; public override bool CanRead => false; }
Our new improved converter extends from JsonConverter<Contact>
. Because of the inherent typed argument, we no longer need CanConvert()
method. More importantly, the method signatures now have strongly-typed Contact
parameters. So we don’t need to worry about explicit casting.
Custom Serialization Using JObject
So far we have used low-level serialization using the writer (JsonWriter
) parameter. While this performs faster, this is not as convenient as it would be with JObject
:
public class SmartContactConverter : JsonConverter<Contact> { public override void WriteJson(JsonWriter writer, Contact? contact, JsonSerializer serializer) { if (contact is null) return; JObject jo = new(); jo[nameof(contact.Name)] = contact.Name; jo[nameof(contact.Department)] = contact.Department.ToString(); if (contact.Department == Department.CustomerCare) { jo[nameof(contact.Phone)] = contact.Phone; } jo[nameof(contact.Address)] = JToken.FromObject(contact.Address, serializer); jo.WriteTo(writer); } // omitted for brevity }
In this smart version of the converter, we first construct a JObject from all our desired properties. We simply populate it with key-value pairs – thanks to JObject
‘s dictionary implementation.
As a plus, we add some extra customizations here. We write the Department
enum as a string. Also, we conditionally expose the Phone
field only for CustomerCare
contacts. And finally, we expose the complex Address
property just by turning it into a JToken
instance.
Once we’re done filling up the JObject
instance, we simply write it to the writer. Using JObject
makes the code more concise and readable but a bit slower than the raw JsonWriter
version.
As we examine the serialized output of the same contacts list using this converter:
[ { "Name": "John", "Department": "Admin", "Address": { "Street": "Street 1", "City": "City 1" } }, { "Name": "Jane", "Department": "CustomerCare", "Phone": "+2341", "Address": { "Street": "Street 2", "City": "City 2" } }, { "Name": "Mike", "Department": "Operations", "Address": { "Street": "Street 3", "City": "City 3" } } ]
We see all our desired customizations are in place!
Custom Deserialization by Overriding ReadJson
It’s time to see how we can customize the deserialization process inside the converter. One good example of such customization is polymorphic deserialization. Let’s consider we have two derived variants of Contact
:
The SuperContact
which provides an extra Mobile
information:
public record class SuperContact( string Name, Department Department, string Phone, Address Address, string Mobile) : Contact(Name, Department, Phone, Address);
And the MasterContact
which additionally shares Email
address:
public record class MasterContact( string Name, Department Department, string Phone, Address Address, string Email) : Contact(Name, Department, Phone, Address);
In polymorphic deserialization, it’s necessary to have a discriminator element in the incoming JSON to distinguish between different object models. Typically this is done by means of $type
metadata and TypeNameHandling settings at both producer and consumer ends. But this approach is not favorable in many situations. For example, we may not have control over how incoming JSON is generated. The end-user may not want JSON output polluted by alien metadata. As this approach does not involve a custom converter, we opt out of exploring it in detail.
A more practical and faster approach would be to utilize the domain knowledge to distinguish between the objects from a JSON graph and deserialize accordingly in a custom converter. In our case, Mobile
node of SuperContact
and Email
node of MasterContact
serve the role of discriminators.
Polymorphic Deserialization Using Custom JsonConverter
So, let’s update our ReadJson
implementation to read different variants of contacts:
public override Contact? ReadJson(JsonReader reader, Type objectType, Contact? existingValue, bool hasExistingValue, JsonSerializer serializer) { JObject jo = JObject.Load(reader); if (jo[nameof(SuperContact.Mobile)] is not null) { var superContact = jo.ToObject<SuperContact>(); return superContact; } if (jo[nameof(MasterContact.Email)] is not null) { var masterContact = jo.ToObject<MasterContact>(); return masterContact; } return jo.ToObject<Contact>(); }
We start by loading the JSON tree into a JObject
instance. From there, we can look for our target discriminator columns. If we find the Mobile
node in the tree, we are certain that it’s representing a SuperContact
object. In that case, we convert the JObject
to SuperContact
by invoking ToObject<SuperContact>()
. Similarly, we deserialize to MasterContact
when Email
node exists. And, when none of the above nodes exists, we simply convert to the base Contact
instance. That’s it.
Let’s finish the setup by turning on the readability mode:
public override bool CanRead => true;
Verify Deserialization of Polymorphic JSON
Our converter is now ready for polymorphic contact deserialization. Let’s first add a sample JSON in DataSource
class:
public static string GetJsonOfMultiVariantContacts() => @"[ { ""Name"": ""John"", ""Department"": ""Admin"", ""Address"": { ""Street"": ""Street 1"", ""City"": ""City 1"" } }, { ""Name"": ""Jane"", ""Department"": ""CustomerCare"", ""Phone"": ""+2341"", ""Address"": { ""Street"": ""Street 2"", ""City"": ""City 2"" }, ""Mobile"": ""0123456"" }, { ""Name"": ""Mike"", ""Department"": ""Operations"", ""Address"": { ""Street"": ""Street 3"", ""City"": ""City 3"" }, ""Email"": ""[email protected]"" } ]";
Once we deserialize this list of multi-variant contacts:
var json = DataSource.GetJsonOfMultiVariantContacts(); var contacts = JsonConvert.DeserializeObject<List<Contact>>(json, new SmartContactConverter())!; Assert.IsType<Contact>(contacts[0]); Assert.IsType<SuperContact>(contacts[1]); Assert.Equal("0123456", ((SuperContact)contacts[1]).Mobile); Assert.IsType<MasterContact>(contacts[2]); Assert.Equal("[email protected]", ((MasterContact)contacts[2]).Email);
We see the sample JSON rightly turns into a list containing a Contact
, a SuperContact
, and a MasterContact
records.
Conclusion
In this article, we have learned how to create a custom JsonConverter in Json.NET. We have also explored how such converters can be handy to achieve polymorphic deserialization.