In this article, we’re going to explore the different flavors of serialization and deserialization in C#.

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

Let’s dive into it.

What Is Serialization and Deserialization in C#?

Serialization is the process of converting the state of an object into a form (string, byte array, or stream) that can be persisted or transported.

Deserialization is the process of converting the serialized stream of data into the original object state. This ensures that the original state is not altered and is recreated when we need it.

Why Do We Need Serialization and Deserialization?

Serialization is useful for the storage and exchange of object states. Within the application memory, the state of an object resides in complicated data structures which is unsuitable for storage or exchange over the network.

So, serialization converts the object into a shareable format.

With serialization, we can transfer objects:

  • Between client and server via REST APIs or GRPC
  • Over the network for messaging systems like Kafka or RabbitMQ
  • Through firewalls as JSON or XML strings
  • To other data stores like SQL or NoSQL databases for persistence

Deserialization is typically useful in reconstructing the object to its original state at its destination for further processing.

Demo Web API

Let’s use a Web API application that performs simple create and read operations on a list of employees:

[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Employee>), StatusCodes.Status200OK)]
public IActionResult GetAllEmployees()
{
    return Ok(_repo.GetAll());
}

[HttpPost]
[ProducesResponseType(typeof(Employee), StatusCodes.Status201Created)]
public IActionResult CreateEmployee(Employee employee)
{
   _repo.Create(employee);
   return Created($"/employees/{employee.Id}", employee);
}

In this example, the Web API exposes two endpoints that get a list of employees and create a new employee respectively.

This Web API supports content transfer in multiple formats (XML, JSON, or Protobuf). The server chooses the data transaction format at runtime depending on the Accept and Content-Type header values in the request:

builder.Services.AddControllers()
    .AddProtobufFormatters()
    .AddXmlSerializerFormatters();

The AddXmlSerializerFormatters() method adds both input and output formatters, so we can serialize objects to and from XML. ASP.NET Core Web API adds the JSON formatter by default. So, there is no need to add it explicitly.

These concepts are part of the broader topic called content negotiation.

We are using the NuGet package WebApiContrib.Core.Formatter.Protobuf to support the Protobuf formatter here:

dotnet add package WebApiContrib.Core.Formatter.Protobuf

Accordingly, the data contract model  contains the protobuf related attributes:

[ProtoContract]
public class Employee
{
     [ProtoMember(1)]
     public Guid Id { get; set; }
        
     [ProtoMember(2)]
     public string Name { get; set; }
        
     [ProtoMember(3)]
     public Address? Address { get; set; }  
}

[ProtoContract]
public class Address
{
     [ProtoMember(1)]
     public string AddressLine1 { get; set; }

     [ProtoMember(2)]
     public string? AddressLine2 { get; set; }

     [ProtoMember(3)]
     public string City { get; set; }

     [ProtoMember(4)]
     public string Country { get; set; }
}

The EmployeeRepository is responsible for interaction with a persistent store for the employees:

public class EmployeeRepository : IEmployeeRepository
{
     private readonly Dictionary<Guid, Employee> _employees = new();
     private Random _rnd = new Random();

     public EmployeeRepository()
     {
         InitializeEmployeeStore();
     }       

     public List<Employee> GetAll()
     {
         return _employees.Values.ToList();
     }

     public void Create(Employee employee)
     {
         if (employee == null)
         {
             return;
         }
         _employees[employee.Id] = employee;
     }
...
}

For simplicity, let’s use an in-memory dictionary as a persistent store for the employees. However, the repositories in an enterprise application will not use an in-memory dictionary. In such cases, it will interact with a relational or non-relational data source.

Let’s send a request to the API endpoint /api/Employees that returns the list of employees. We set the Accept header value as application/json:

curl -X 'GET' \
  'https://localhost:7181/api/Employees' \
  -H 'accept: application/json'

Hence, we get the response in JSON format:

[
  {
    "id": "4d0b2bd1-f611-453b-be0c-b7b04578893d",
    "name": "TestEmployee1",
    "address": {
      "addressLine1": "Street #1",
      "addressLine2": "District #1",
      "city": "Sample City3",
      "country": "Sample Country1"
    }
  },
  {
    "id": "1e071d0e-b1d7-4a06-ad3d-2fa8ad3640fb",
    "name": "TestEmployee2",
    "address": {
      "addressLine1": "Street #2",
      "addressLine2": "District #2",
      "city": "Sample City4",
      "country": "Sample Country3"
    }
  }, 
 ...
 ...
]

Similarly, we set the Accept header as application/xml in the request header:

curl -X 'GET' \
  'https://localhost:7181/api/Employees' \
  -H 'accept: application/xml'

Consequently, we get the response in XML format:

<ArrayOfEmployee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Employee>
    <Id>4d0b2bd1-f611-453b-be0c-b7b04578893d</Id>
    <Name>TestEmployee1</Name>
    <Address>
      <AddressLine1>Street #1</AddressLine1>
      <AddressLine2>District #1</AddressLine2>
      <City>Sample City3</City>
      <Country>Sample Country1</Country>
    </Address>
  </Employee>
  <Employee>
    <Id>1e071d0e-b1d7-4a06-ad3d-2fa8ad3640fb</Id>
    <Name>TestEmployee2</Name>
    <Address>
      <AddressLine1>Street #2</AddressLine1>
      <AddressLine2>District #2</AddressLine2>
      <City>Sample City4</City>
      <Country>Sample Country3</Country>
    </Address>
  </Employee>
  ...
  ...
</ArrayOfEmployee>

Finally, let’s see how the response looks when we set the Accept header as application/x-protobuf:

curl -X 'GET' \
  'https://localhost:7181/api/Employees' \
  -H 'accept: application/x-protobuf'

However, please note that the response in protobuf format is not human-readable:

\
	�+M�;E���Ex�=
TestEmployee17
    Street #1District #1Sample City3"Sample Country1
\
	ױJ�=/��[email protected]�
TestEmployee27
    Street #2District #2Sample City4"Sample Country3
\
	T��,ϮE��%���'
TestEmployee37
    Street #3District #3Sample City2"Sample Country3
\
	�e#*l�dF��-�[email protected]�
TestEmployee47
    Street #4District #4Sample City3"Sample Country3
\
	Ɨu���KL�.��ֹX
TestEmployee57
    Street #5District #5Sample City2"Sample Country3

Consuming the API

Let’s use a .NET console application as a consumer of the Web API described in the previous section.

The client application selects JSON, XML, or Protobuf for the exchange of data with the web API based on user input:

IClient client = format switch
{
    "json" => new JsonClient(),
    "cache" => new CachedJsonClient(),
    "xml" => new XmlClient(),
    "proto" => new ProtobufClient(),
    _ => new JsonClient()
};

Depending on the functional requirements, we may need to serialize a POCO (Plain Old CLR Object)  into a string or an array of bytes. For example, we may need to serialize a complex object into a JSON string and store it into a NoSQL DB like MongoDB or Redis.

There can also be a scenario where we use RabbitMQ for asynchronous communication between two service components. RabbitMQ doesn’t allow plain strings or complex types in the message body. In this case, we need the transfer object to be converted to byte[].

For such use cases, the client code supports string as well as byte array serialization and deserialization. This is indicated by the interfaces IStringSerializer and IByteSerilizer.

The IStringSerilizer implementation serializes a POCO to a string. Also, it can de-serialize a string to a POCO:

internal interface IStringSerializer    
{
     T Deserialize<T>(string text);

     string Serialize(object data);
}

Similarly, the IByteSerializer serializes a POCO to an array of bytes and vice-versa:

internal interface IByteSerializer
{
     T Deserialize<T>(byte[] buffer);

     byte[] Serialize(object data);
}

We will explore the concrete implementations later on.

Irrespective of the format chosen by the client, we always see two lists of employees in the output. One before and the other after adding a new employee which includes the newly added employee:

Enter a format name(json, cache, xml, proto): json

Employee list before adding an employee.
--------------------------------------
Name:TestEmployee1
Address:Street #1, District #1, Sample City3, Sample Country1

Name:TestEmployee2
Address:Street #2, District #2, Sample City4, Sample Country3

Name:TestEmployee3
Address:Street #3, District #3, Sample City2, Sample Country3

Name:TestEmployee4
Address:Street #4, District #4, Sample City3, Sample Country3

Name:TestEmployee5
Address:Street #5, District #5, Sample City2, Sample Country3

New employee TestEmployee6 has been added

Employee list after adding an employee.
--------------------------------------
Name:TestEmployee1
Address:Street #1, District #1, Sample City3, Sample Country1

Name:TestEmployee2
Address:Street #2, District #2, Sample City4, Sample Country3

Name:TestEmployee3
Address:Street #3, District #3, Sample City2, Sample Country3

Name:TestEmployee4
Address:Street #4, District #4, Sample City3, Sample Country3

Name:TestEmployee5
Address:Street #5, District #5, Sample City2, Sample Country3

Name:TestEmployee6
Address:Street #6, District #6, Sample City30, Sample Country22

JSON Serialization

JSON serialization converts the public properties of an object into a string, byte array, or stream that conforms to the RFC 8259 JSON specification

In this demo, the JsonStringSerializer class implements the IStringSerializer interface. This wrapper class uses the JsonSerializer available in System.Text.Json to serialize and deserialize an object:

internal class JsonStringSerializer : IStringSerializer
{
     public T Deserialize<T>(string text)
         => JsonSerializer.Deserialize<T>(text, _options);

     public string Serialize(object data)
         => JsonSerializer.Serialize(data, _options);
}

JsonStringSerializer also supports the passing of different serialization options through a parameterized constructor:

private readonly JsonSerializerOptions _options;

public JsonStringSerializer()
    : this(GetDefaultJsonSerializerOptions())
{
}

public JsonStringSerializer(JsonSerializerOptions options)
     => _options = options;

Let’s now see how this JsonStringSerializer is used in the JsonClient.

The GetAllEmployees() method uses the Deserialize() method in JsonStringSerializer class to convert the Web API response to a List:

public async Task<List<Employee>> GetAllEmployees()
{
      var url = $"{BaseUrl}/api/employees";
      var request = new HttpRequestMessage(HttpMethod.Get, url);
      request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
      
      var response = await _client.SendAsync(request);
      var content = await response.Content.ReadAsStringAsync();
      
      return _serializer.Deserialize<List<Employee>>(content);
}

We set the Accept header as application/json here to instruct the server to return the response data in a format that conforms to JSON specifications. We use the ReadAsStringAsync() method to convert the HttpContent instance to a JSON string. Now, the code deserializes the resulting JSON to the intended C# object.

As the name suggests, the CreateEmployee() method sends an employee object to the Web API to create a new employee at the server:

public async Task<Employee> CreateEmployee(Employee employee)
{
      var url = $"{BaseUrl}/api/employees";
      var request = new HttpRequestMessage(HttpMethod.Post, url);
      request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));            
      request.Content = new StringContent(_serializer.Serialize(employee), Encoding.UTF8,"application/json");
      
      var response = await _client.SendAsync(request);

      if (response.IsSuccessStatusCode)
      {
          var newEmployee = _serializer.Deserialize<Employee>(await response.Content.ReadAsStringAsync());
          return newEmployee;
      }

      return null;
}

Firstly, the Content-Type header is set as application/json to indicate the format used by the object in the request payload for content negotiation between the client and the server. Then, the Serialize() method serializes the new Employee POCO to a JSON string which is then used as StringContent in the HttpRequestMessage object.

Using JSON Serialization in Caching

Let’s explore a use case where the caching mechanism uses JSON Serialization.

The IDistributedCache interface available in Microsoft.Extensions.Caching.Distributed package is used to read and write data from an underlying cache-store. The GetAsync() and SetAsync() methods in the interface only support reading and writing a byte to the underlying cache. This is where we will be using the JsonByteSerializer.

The JsonByteSerializer class implements the IByteSerializer. This class also uses the same JsonSerializer but with minor tweaks to serialize and deserialize POCOs to byte[] instead of string and vice versa:

internal class JsonByteSerializer : IByteSerializer
{
     private readonly JsonSerializerOptions _options;

     public T Deserialize<T>(byte[] buffer)
     {
         using var stream = new MemoryStream(buffer);           
         return JsonSerializer.Deserialize<T>(stream, _options);
     }

      public byte[] Serialize(object data)
      {
            return JsonSerializer.SerializeToUtf8Bytes(data, _options);
      }   
}

The CachedJsonClient uses the JsonByteSerializer to get a list of employees and also create a new employee. But, it also uses a caching mechanism to improve performance:

public async Task<List<Employee>> GetAllEmployees()
{
       // Find cached item
       byte[] objectFromCache = await _distributedCache.GetAsync(AllEmployeesKey);

       if (objectFromCache != null)
       {                
           var employees = _serializer.Deserialize<List<Employee>>(objectFromCache);
           return employees;
       }

       return await RefreshCache();
}

The GetAllEmployees() method reads the list of employees from the cache. If it can read the data from the cache, it deserializes the byte[] retrieved from cache to a List using the Deserialize() method from JsonByteSerializer.

private async Task<List<Employee>> RefreshCache()
{
       // If not found, then recalculate response
       var content = await GetResponseFromApi();

       var cacheEntryOptions = new DistributedCacheEntryOptions()
           .SetSlidingExpiration(TimeSpan.FromSeconds(10))
           .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

       // Cache it
       var resultAsByteArray = _serializer.Serialize(await content.ReadAsStringAsync());
       await _distributedCache.SetAsync(AllEmployeesKey, resultAsByteArray, cacheEntryOptions);

       return _serializer.Deserialize<List<Employee>>(resultAsByteArray);
}

private async Task<HttpContent> GetResponseFromApi()
{
       var url = $"{BaseUrl}/api/employees";
       var request = new HttpRequestMessage(HttpMethod.Get, url);
       request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
       
       var response = await _client.SendAsync(request);
       
       return response.Content;           
}

If there is a cache miss, the GetAllEmployees() method gets the data from the Web API and saves it in the cache. The Serialize() method from JsonByteSerializer converts the JSON string response from Web API to a byte[] before saving it in the cache.

To learn more details about JSON Serialization using the System.Text.Json library, you can check our article on Introduction to System.Text.Json Through Examples.

XML Serialization

Similar to JSON serialization, XML serialization and deserialization is the process of conversion of C# objects to XML and vice versa. The custom XmlSerializer class used in this demo implements the IStringSerializer interface. It is a wrapper over the System.Xml.Serialization.XmlSerializer provided by .NET out of the box:

internal class XmlSerializer : IStringSerializer
{
    public T Deserialize<T>(string text)
    {
        var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T));

         using (var stream = new StringReader(text))
         {
             return (T)serializer.Deserialize(stream);
         }
     }

     public string Serialize(object data)
     {
         var serializer = new System.Xml.Serialization.XmlSerializer(data.GetType());

         using (var stream = new Utf8StringWriter())
         {
             serializer.Serialize(stream, data);
             return stream.ToString();
         }
     }
}

The Serialize() method uses the StringWriter class from the System.IO namespace in conjunction with the out-of-the-box XML serializer to create an XML representation of the object as a string. Similarly, the Deserialize() method uses the StringReader to create an object by reading the string representation of an XML.

The XmlClient is very similar to the JsonClient. However, here, the Accept and Content-Type headers are set as application/xml instead of application/json.

request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));

request.Content = new StringContent(_serializer.Serialize(employee), Encoding.UTF8, "application/xml");
To know in details about XML Serialization, you can check our article on Serializing Objects to XML in C#.

Protobuf Serialization

Google developed Protocol Buffers or ProtoBuf for communication between their service components. It is a method of encoding and sending out structured data in an efficient manner. Protobuf is language-neutral, platform-neutral, and highly performant.

In this demo, the ProtobufSerializer class implements the IByteSerializer and acts as a wrapper over the Serializer class provided by the protobuf-net assembly. The protobuf-net is a contract-based serializer for the .NET code. This was installed along with the WebApiContrib.Core.Formatter.Protobuf package that we have introduced in the Demo Web API section.

The native protobuf serializer only serializes to and deserializes from the .NET Stream type. Hence, the ProtobufSerializer methods use a MemorStream in conjunction with the out of the protobuf Serializer to convert POCOs to byte[] and vice-versa:

internal class ProtobufSerializer : IByteSerializer
{
     public T Deserialize<T>(byte[] buffer)
     {
         using (var stream = new MemoryStream(buffer))
        {
             return Serializer.Deserialize<T>(stream);
        }
     }

     public byte[] Serialize(object data)
     {
         using (var stream = new MemoryStream())
         {
             Serializer.Serialize(stream, data);
             stream.Flush();

             return stream.ToArray();
          }
     }
}

Since the data encoded in protobuf format is not human readable, the IByteSerializer is a better fit in this case compared to the IStringSerializer. We could have used MemoryStream directly as well in place of byte[], But we are maintaining consistency here by implementing the IByteSerializer.

In the next section, we will learn why Protobuf has become popular in recent times over the standard formatting like JSON or XML.

Let’s see how the ProtobufSerializer is used in the ProtobuClient:

public async Task<List<Employee>> GetAllEmployees()
{
      var url = $"{BaseUrl}/api/employees";
      var request = new HttpRequestMessage(HttpMethod.Get, url);
      request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-protobuf"));
      
      var response = await _client.SendAsync(request);
      var content = await response.Content.ReadAsByteArrayAsync();
      
      return _serializer.Deserialize<List<Employee>>(content);
}

Similar to the JsonClient, The GetAllEmployees() method in ProtobufClient uses the Deserialize() method in the ProtobufSerializer class to convert the Web API response to a List. The only differences are that the Accept header is set as application/x-protobuf as part of content negotiation with the Web API. Also, the HttpConetent instance is read as byte[] from the response.

The ProtobufClient also has a CreateEmployee() method to create a new employee:

public async Task<Employee> CreateEmployee(Employee employee)
{
      var url = $"{BaseUrl}/api/employees";
      var request = new HttpRequestMessage(HttpMethod.Post, url);
      request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-protobuf"));            
      request.Content = new ByteArrayContent(_serializer.Serialize(employee));
      request.Content.Headers.Add("Content-Type", "application/x-protobuf");
      
      var response = await _client.SendAsync(request);

      if (response.IsSuccessStatusCode)
      {
           var newEmployee = _serializer.Deserialize<Employee>(await response.Content.ReadAsByteArrayAsync());
           return newEmployee;
      }

       return null;
}

Here, the Content-Type header is set as application/x-protobuf to indicate the format used by the object in the request payload for communication between the client and the server. The Serialize() method from the ProtobufSerializer serializes the new employee POCO to a byte[] which is then used as ByteArrayContent in the HttpRequestMessage object.

Why Is Protobuf Serialization Gaining Popularity

To date, JSON remains the most popular format to communicate over HTTP. JSON has many obvious advantages as a data-interchange format. It is human readable, well understood, and typically performs well. But, in recent times protocol buffers have become increasingly popular.

Text-based serializers like JSON and XML have certain disadvantages:

  • They require complex parsing code to conform to human-readable options
  • They need more bandwidth as the property names also need to be part of the payload

Protocol buffers use a binary format. Furthermore, the message does not include the metadata defining the message content. For example, if our message has a property named EmployeeName then this name is not part of the message. In XML and JSON we will include EmployeeName as a string for each occurrence of the property in the message. Hence, protocol buffer messages are more compact in comparison to the same messages in XML or JSON.

Protobuf is considered ideal for internal API communications between two server components within a distributed architecture where it helps in optimizing performance over low bandwidth networks.

There are situations when JSON/XML is a better fit than something like Protocol Buffers, including situations where:

  • The data needs to be human-readable
  • A web browser consumes response data from a service
  • Our server application uses JavaScript

Each serialization format has its advantages and disadvantages. It depends on the context it is used.

For example, in an on-premises data center with low bandwidth, services can communicate using protobuf. Whereas, we cannot dispense with JSON when we build our web client application using JS frameworks like Angular/React. We may also need XML serialization to make a system backward compatible with a client that is in a legacy platform.

Conclusion

In this article, we have covered the different use cases where serialization and deserialization in C# play a pivotal role.

It is important to understand that we cannot consider serialization as a one-size-fits-all approach. It depends on the usage and the different components that are involved in the systems that communicate among each other.