Avoid conversion errors by using Custom Converters in System.Text.Json API(.NET Core 3.0)

The post is based on .NET Core 3.0
SDK used : 3.0.100

One of the most common error encountered while doing JSON serialization and deserialization is the data type conversion errors. Till now, we were using the Json.NET library from Newtonsoft for performing the serialization and deserialization in .NET/ASP.NET/.NET Core, but in the latest iteration of .NET Core which is currently under preview, they have removed the dependency on Json.NET and introduced a new built-in library for doing the same. Along with that, the all new library that is going to be introduced with .NET Core 3.0 provides you to define custom converters that can be implemented to get rid of this kind of errors. If you are not aware of it, I have already written a couple of posts about it which you can refer to using the following links.

 Serializing and Deserializing Json in .NET Core 3.0 using System.Text.Json API

Step-By-Step Guide to Serialize and Deserialize JSON Using System.Text.Json

The new API is included with the System namespace and you don't need to add any NuGet package to get started with it. Along with the normally used methods for serializing/deserializing JSON, it also includes methods for supporting asynchronous programming. One of the known limitation in the v1 of the API is the limited support for the data types, given below is the list of currently supported types. For more detail, please refer this link Serializer API Document

  • Array 
  • Boolean
  • Byte
  • Char (as a JSON string of length 1)
  • DateTime 
  • DateTimeOffset 
  • Dictionary<string, TValue> (currently just primitives in Preview 5)
  • Double
  • Enum (as integer for now)
  • Int16
  • Int32
  • Int64
  • IEnumerable 
  • IList 
  • Object (polymorhic mode for serialization only)
  • Nullable < T >
  • SByte
  • Single
  • String
  • UInt16
  • UInt32
  • UInt64

Also, by design, any support for type coercion/inference is not included by default. For example, if we are trying to convert a boolean value stored as a string in the JSON to a boolean type, the serializer will throw an error during the deserializing operation. To illustrate this, let's consider the below JSON

[
    {
        "Id": 1026,
        "Name": "Echo Dot",
        "Description": "Smart speakers from Amazon",
        "IsInStock": "false"
    },
    {
        "Id": 8084,
        "Name": "Chromecast",
        "Description": "Stream content to TV",
        "IsInStock": "true"
    },
    {
        "Id": 9096,
        "Name": "iPhone",
        "Description": "Latest one from Apple",
        "IsInStock": "false"
    }
]

And I have defined a class like the one below 

class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime LastUpdatedOn { get; set; }
        public bool IsInStock { get; set; }
    }

If we try to deserialize using the following statement, the API will throw an error as shown below

JsonSerializer.Deserialize<List<Product>>(productList, options);
Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to System.Boolean. Path: $[0].IsInStock | LineNumber: 5 | BytePositionInLine: 25.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'String' as a boolean.

In the JSON, for the "IsInStock" attribute the value is stored as a string and we are trying to map that attribute to a boolean property in the Product class using the Deserialize method in the API.

Complete code 

ConverterSample.cs

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Collections.Generic;

namespace JsonTextApi
{

    class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime LastUpdatedOn { get; set; }
        public bool IsInStock { get; set; }
    }
    class ConverterSample
    {
        public List<Product> DeserializeData(string productList)
        {
           
            return JsonSerializer.Deserialize<List<Product>>(productList);
        }
        

    }
}

Program.cs

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.IO;
using System.Collections.Generic;

namespace JsonTextApi
{
    class Program
    {
        static void Main(string[] args)
        {
            
            ConverterSample obj = new ConverterSample();
            var objProduct = new Product();

            var productJson = File.ReadAllText("input.json");
            Console.WriteLine(productJson);
            Console.ReadLine();
            var items = obj.DeserializeData(productJson);
            
            
            foreach(var item in items)
            {
                Console.WriteLine($"{item.Id}, {item.Name}, {item.IsInStock}");
            }
            Console.ReadLine();            
        }

    }
}

This is happening because, as I already mentioned the API doesn't do these sort of conversions by default. But in most of the circumstances, the consuming applications don't have control over the type or format of the data. So to handle this kind of situations, one may need to implement custom converters to handle the job for you. It's very to easy to create and hookup converters using the System.Text.Json API. 

Using Custom Converters

Let's create a custom converter to convert the value stored as string in the JSON to a boolean type.

Step 1: Create a class by inheriting from JsonConverter<T> class available in the System.Text.Json.Serialization namespace where T is the type you want to convert to. 

public class BooleanConverter : JsonConverter<bool>

Step 2: Override the Read method to handle the deserialization of the incoming JSON string. Here, the conversion will happen if the value in the string is either "True", "true", "1", "False", "false", "0", the first three will resolve to boolean true and rest to boolean false

        public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            string value = reader.GetString();
            string chkValue = value.ToLower();
            if (chkValue.Equals("true") ||chkValue.Equals("yes") || chkValue.Equals("1") )
            {
                return true;
            }
            if (value.ToLower().Equals("false") ||chkValue.Equals("no") || chkValue.Equals("0"))
            {
                return false;
            }
            throw new JsonException();

        }

Step 3: Similarly, Override the Write method if you want to convert the boolean value to a string while doing serialization

 public override void Write( Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        {
            switch (value)
            {
                case true:
                    writer.WriteStringValue("true");
                    break;
                case false:
                    writer.WriteStringValue("false");
                    break;
              
            }

        }

Step 4: Wire our converter method to the API. We can do that in multiple ways, either we can register the converter through JsonSerializerOptionsor by placing the [JsonConverter] on the property as shown below.

var options  = new JsonSerializerOptions();
options.Converters.Add(new BooleanConverter());

or

[JsonConverter(typeof(BooleanConverter))]
public bool IsInStock { get; set; }

Let's go for the first approach and see how it solves the problem. Let's modify the DeserializeData method shown in the first part as below

        public List<Product> DeserializeData(string productList)
        {
             var options  = new JsonSerializerOptions();
             options.Converters.Add(new BooleanConverter());

            return JsonSerializer.Deserialize<List<Product>>(productList,options);
        }

Here's the code in full

ConverterSample.cs

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Collections.Generic;

namespace JsonTextApi
{

    class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime LastUpdatedOn { get; set; }
        public bool IsInStock { get; set; }
    }
    class ConverterSample
    {
        public List<Product> DeserializeData(string productList)
        {
             var options  = new JsonSerializerOptions();
             options.Converters.Add(new BooleanConverter());

            return JsonSerializer.Deserialize<List<Product>>(productList,options);
        }
        public string SerializeData(List<Product>  productList)
        {
            var options  = new JsonSerializerOptions() {WriteIndented = true };
            options.Converters.Add(new BooleanConverter());

            return JsonSerializer.Serialize<List<Product>>(productList, options);
        }

    }
}

Program.cs

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.IO;
using System.Collections.Generic;

namespace JsonTextApi
{
    class Program
    {
        static void Main(string[] args)
        {
            ConverterSample obj = new ConverterSample();
            var objProduct = new Product();
            //input json is read from the file
            var productJson = File.ReadAllText("input.json");
            Console.WriteLine(productJson);
            Console.ReadLine();
            var items = obj.DeserializeData(productJson);
            
            foreach(var item in items)
            {
                Console.WriteLine($"{item.Id}, {item.Name}, {item.IsInStock}");
            }

        }

    }
}


Output

INPUT STRING
=====================
[
    {
        "Id": 1026,
        "Name": "Echo Dot",
        "Description": "Smart speakers from Amazon",
        "IsInStock": "no"
    },
    {
        "Id": 8084,
        "Name": "Chromecast",
        "Description": "Stream content to TV",
        "IsInStock": "yes"
    },
    {
        "Id": 9096,
        "Name": "iPhone",
        "Description": "Latest one from Apple",
        "IsInStock": "no"
    }
]

DESERIALIZED DATA
=====================
1026, Echo Dot, False
8084, Chromecast, True
9096, iPhone, False

Similarly, for serializing the data, you can hook up the convertor to JsonSerializerOptions object and pass it on to the Serialize method. Please refer to the SerializeData method in the ConverterSample.cs give above for the implementation.

Part 1 : Serializing and Deserializing Json in .NET Core 3.0 using System.Text.Json API

Part 2: Step-By-Step Guide to Serialize and Deserialize JSON Using System.Text.Json


No Comments

Add a Comment