In this post, we are going to learn how to create a custom JsonConverter in Json.NET.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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. 

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!