Serialization and deserialization of objects to JSON are common tasks in .NET projects. In the .NET ecosystem, System.Text.Json stands out as a native, efficient, and flexible library for handling JSON. While widely used for simple cases, complex situations often arise, such as working with custom date and time formats or dealing with enums represented as strings in JSON. In this article, we’ll explore how System.Text.Json can be adapted to meet these demands, focusing on creating and using custom converters.
Introduction to System.Text.Json
Before diving into advanced techniques, it’s important to understand how basic serialization and deserialization work with System.Text.Json. This library natively supports converting C# objects to JSON and vice-versa. Let’s see the example below:
using System.Text.Json;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
var person = new Person { Name = "Jhon", Age = 30 };
// Serialization: object to JSON
string json = JsonSerializer.Serialize(person);
Console.WriteLine(json);
//Result: {"Name":"Jhon","Age":30}
// Deserialization: JSON to object
var deserialized = JsonSerializer.Deserialize(json);
Console.WriteLine($"{deserialized.Name}, {deserialized.Age}");
//Result: Jhon, 30
In this example, System.Text.Json automatically uses the class property names to map fields in the JSON. This behavior is convenient when the JSON format directly matches the class structure.
Now that we understand the basics, let’s explore how to handle more complex situations.
Custom converters
Custom converters are useful when:
- The JSON format does not directly match the class properties.
- Special values need to be handled, such as enums represented by strings or specific date formats.
- The JSON contains dynamic or missing properties.
Creating a Custom Converter
Custom converters are implemented by extending the JsonConverter<T> class. To use a converter, you register it at the property level (with [JsonConverter]) or globally in the serializer options.
Imagine a system that uses the date format "dd/MM/yyyy". A custom converter can map this format to DateTime.
Json:
{ "Customer": "Maria", "DeliveryDate": "25/12/1990" }
To deserialize this JSON into an object, let’s create a new class called Order:
public class Order
{
public string Customer { get; set; }
[JsonConverter(typeof(DateFormatConverter))]
public DateTime DeliveryDate { get; set; }
}
The Order class has the properties Customer, a simple string field that will be automatically mapped to JSON, and DeliveryDate, a DateTime field that uses the [JsonConverter] attribute to associate a custom converter, DateFormatConverter, which will be created below.
This attribute is necessary because the default serialization format for DateTime in System.Text.Json uses ISO 8601 (“yyyy-MM-ddTHH:mm:ss.fffZ”), which does not match the specific format (“dd/MM/yyyy”) used in this example.
Now let’s create the custom converter:
public class DateFormatConverter : JsonConverter
{
private const string DateFormat = "dd/MM/yyyy";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.ParseExact(reader.GetString(), DateFormat, null);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(DateFormat));
}
}
The DateFormatConverter class extends the behavior of System.Text.Json by serializing and deserializing dates in the "dd/MM/yyyy" format. It inherits from JsonConverter<DateTime> and implements two main methods:
- Read (Deserialization): This method is called when a JSON is being converted into a C# object. It uses
Utf8JsonReaderto read the property value in JSON and thenDateTime.ParseExactto interpret the string in the specific format and transform it into aDateTimeobject. - Write (Serialization): During serialization, this method is called to convert a
DateTimevalue from the C# object into a JSON-formatted string. The method usesUtf8JsonWriterand callsvalue.ToString(DateFormat)to ensure the output format matches the expected one.
Here is how we can test it:
var json = @"{ ""Customer"": ""Jhon"", ""DeliveryDate"": ""15/12/2024"" }";
var order = JsonSerializer.Deserialize(json);
Console.WriteLine($"{order.Customer} - {order.DeliveryDate:yyyy-MM-dd}");
Handling Enums as Strings
Enums can be represented as custom strings in JSON, and a converter facilitates this translation, allowing enum values to be manipulated more flexibly when serializing or deserializing objects.
public enum Status
{
ACtive,
Inactive
}
public class StatusConverter : JsonConverter
{
public override Status Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetString() switch
{
"active" => Status.Active,
"inactive" => Status.Inactive,
_ => throw new JsonException("Invalid value")
};
}
public override void Write(Utf8JsonWriter writer, Status value, JsonSerializerOptions options)
{
writer.WriteStringValue(value == Status.Ativo ? "active" : "inactive");
}
}
In the above code, the Read method checks which value in JSON matches the defined cases in the switch statement and returns the corresponding enum value. The Write method converts the enum value into a string to be written in JSON.
Now, let’s create a new User class with a Status property using the custom converter:
public class User
{
[JsonConverter(typeof(StatusConverter))]
public Status Status { get; set; }
}
Now we can validate this implementation:
var userJson = @"{ ""Status"": ""active"" }";
var user = JsonSerializer.Deserialize(userJson);
Console.WriteLine(user.Status);
In this example, the JSON with the string "active" is converted to the value Status.Active in the Status property of the Userobject. This process enables flexible mapping between enum values and their string representations in JSON, making it easier to integrate with systems that use specific formats to represent enums.
Handling Dynamic Data
In many cases, when working with JSON data, the structure may be unpredictable or vary according to different scenarios, such as when consuming external APIs or dealing with third-party systems. In these cases, it is not practical or possible to define classes beforehand to represent all possible JSON formats.
For such scenarios, System.Text.Json provides tools like JsonElement and JsonDocument, which allow accessing and manipulating data dynamically without the need for specific models.
To better understand this, imagine you’re consuming an API that returns order information, but the JSON format may vary depending on the order items or system updates.
var json = @"
{
""OrderId"": 12345,
""Customer"": ""Jhon"",
""Items"": [
{ ""Product"": ""Laptop"", ""Quantity"": 1, ""Price"": 3500.00 },
{ ""Product"": ""Mouse"", ""Quantity"": 2, ""Price"": 150.00 }
],
""Date"": ""2024-12-16"",
""Status"": ""Sent""
}";
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
// Acessando propriedades principais
Console.WriteLine($"Order ID: {root.GetProperty("OrderId").GetInt32()}");
Console.WriteLine($"Customer: {root.GetProperty("Customer").GetString()}");
Console.WriteLine($"Date: {root.GetProperty("Date").GetString()}");
Console.WriteLine($"Status: {root.GetProperty("Status").GetString()}");
// Acessando a lista de itens
Console.WriteLine("\nOrder items:");
foreach (var item in root.GetProperty("Items").EnumerateArray())
{
var product = item.GetProperty("Product").GetString();
var quantity = item.GetProperty("Quantity").GetInt32();
var price = item.GetProperty("Price").GetDecimal();
Console.WriteLine($"- {product}: {quantity} x {price:F2}");
}
}
In this scenario, JsonDocument.Parse(json) parses the JSON string and creates a JsonDocument object to access its structure. After that, GetProperty() is used to access values such as OrderId, Customer, Date, and Status. These methods return a JsonElement, from which we can extract values using methods like GetString(), GetInt32(), and GetDecimal().
The console output will be:
Order ID: 12345
Customer: Jhon
Date: 2024-12-16
Status: Sent
Order Items:
- Laptop: 1 x 3500,00
- Mouse: 2 x 150,00
Conclusion
System.Text.Json offers flexibility and great performance for handling JSON in .NET applications. With custom converters, you can adapt JSON processing to more complex needs, ensuring your systems remain robust and efficient. Try out the techniques presented in this article to master advanced JSON manipulation!