Mastering JSON Serialization in C# with System.Text.Json

Madhawa Polkotuwa
8 min readSep 27, 2024

--

Customize and Control JSON Output with JsonSerializerOptions for Cleaner, Efficient Data Handling

intro

In this post, I’ll dive into JSON serialization in C# using the System.Text.Json library. I will cover how to serialize and deserialize objects, handle special cases like null values, format dates, and customize the output using JsonSerializerOptions.

Import the necessary namespaces. This includes System.Text.Json for JSON processing and System.Text.Json.Serialization for handling advanced options like converters and reference handling."

using System.Text.Json;
using System.Text.Json.Serialization;

Define a simple Product class, which includes properties like ID, ProductName, Category, Price, and PurchasedDate. This class represents the objects that will be serialized into JSON.

public class Product
{
public int ID {get; set;}
public string ProductName {get; set;}
public string Category {get; set;}
public decimal Price {get; set;}
public DateTime PurchasedDate {get; set;}
}

Creating a List of Products:

Now, Let’s create a list of Product objects with some sample data. This data will demonstrate how JSON serialization handles different data types, including strings, numbers, and dates. One of the products has a null value for the Category, which will help us showcase how JsonSerializer deals with null values.

List<Product> products = new List<Product>
{
new Product { ID = 2, ProductName = "Harry Potter", Category = "Books", Price = 24.99m, PurchasedDate = new DateTime(2024,09,24,10,20,30) },
new Product { ID = 3, ProductName = "Console", Category = "Electronics", Price = 199.99m, PurchasedDate = new DateTime(2024,09,25,12,22,45) },
new Product { ID = 4, ProductName = "Pen", Category = null, Price = 10.0m, PurchasedDate = new DateTime(2024,09,25,06,10,15) }, // Category is null
new Product { ID = 5, ProductName = "TShirt", Category = "Clothing", Price = 49.99m, PurchasedDate = new DateTime(2024,09,27,08,12,20) },
new Product { ID = 1, ProductName = "Laptop", Category = "Electronics", Price = 299.99m, PurchasedDate = DateTime.Now }
};

Default JSON Serialization

Let’s start by serializing the list of products using the default settings. This will convert the object graph into a JSON string, automatically handling types like strings, decimals, and dates

string jsonString = JsonSerializer.Serialize(products);
jsonString
[{"ID":2,"PrductName":"Harry Potter","Category":"Books","Price":24.99,"PurchasedDate":"2024-09-24T10:20:30"},{"ID":3,"PrductName":"Console","Category":"Electronics","Price":199.99,"PurchasedDate":"2024-09-25T12:22:45"},{"ID":4,"PrductName":"Pen","Category":null,"Price":10.0,"PurchasedDate":"2024-09-25T06:10:15"},{"ID":5,"PrductName":"TShirt","Category":"Clothing","Price":49.99,"PurchasedDate":"2024-09-27T08:12:20"},{"ID":1,"PrductName":"Laptop","Category":"Electronics","Price":299.99,"PurchasedDate":"2024-09-27T09:55:51.3731903+09:00"}]

Controlling Serialization with JsonSerializerOptions

Pretty Printing (WriteIndented)

  • WriteIndented = true can make the output JSON human-readable.
options = new JsonSerializerOptions { WriteIndented = true };
json = JsonSerializer.Serialize( products, options);
json
[
{
"ID": 2,
"PrductName": "Harry Potter",
"Category": "Books",
"Price": 24.99,
"PurchasedDate": "2024-09-24T10:20:30"
},
{
"ID": 3,
"PrductName": "Console",
"Category": "Electronics",
"Price": 199.99,
"PurchasedDate": "2024-09-25T12:22:45"
},
{
"ID": 4,
"PrductName": "Pen",
"Category": null,
"Price": 10.0,
"PurchasedDate": "2024-09-25T06:10:15"
},
{
"ID": 5,
"PrductName": "TShirt",
"Category": "Clothing",
"Price": 49.99,
"PurchasedDate": "2024-09-27T08:12:20"
},
{
"ID": 1,
"PrductName": "Laptop",
"Category": "Electronics",
"Price": 299.99,
"PurchasedDate": "2024-09-27T09:55:51.3731903+09:00"
}
]

Ignoring Null Values

  • Ignore null properties during serialization using DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull.
options = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
json = JsonSerializer.Serialize(products, options);
json
[
{
"ID": 2,
"PrductName": "Harry Potter",
"Category": "Books",
"Price": 24.99,
"PurchasedDate": "2024-09-24T10:20:30"
},
{
"ID": 3,
"PrductName": "Console",
"Category": "Electronics",
"Price": 199.99,
"PurchasedDate": "2024-09-25T12:22:45"
},
{
"ID": 4,
"PrductName": "Pen", /* In this case Category is null so it won't appear in the JSON output. */
"Price": 10.0,
"PurchasedDate": "2024-09-25T06:10:15"
},
{
"ID": 5,
"PrductName": "TShirt",
"Category": "Clothing",
"Price": 49.99,
"PurchasedDate": "2024-09-27T08:12:20"
},
{
"ID": 1,
"PrductName": "Laptop",
"Category": "Electronics",
"Price": 299.99,
"PurchasedDate": "2024-09-27T09:55:51.3731903+09:00"
}
]

Property Name Case Customization

Change the property naming policy, such as using CamelCase or keeping original names.

options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
json = JsonSerializer.Serialize(products, options);
json
[
{
"id": 2,
"prductName": "Harry Potter",
"category": "Books",
"price": 24.99,
"purchasedDate": "2024-09-24T10:20:30"
},
{
"id": 3,
"prductName": "Console",
"category": "Electronics",
"price": 199.99,
"purchasedDate": "2024-09-25T12:22:45"
},
{
"id": 4,
"prductName": "Pen",
"category": null,
"price": 10.0,
"purchasedDate": "2024-09-25T06:10:15"
},
{
"id": 5,
"prductName": "TShirt",
"category": "Clothing",
"price": 49.99,
"purchasedDate": "2024-09-27T08:12:20"
},
{
"id": 1,
"prductName": "Laptop",
"category": "Electronics",
"price": 299.99,
"purchasedDate": "2024-09-27T09:55:51.3731903+09:00"
}
]

Custom naming Policy

You can also implement a custom JsonNamingPolicy if you need more control over the transformation of dictionary keys.

public class UpperCaseNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name)
{
return name.ToUpper(); // Convert keys to uppercase
}
}

options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = new UpperCaseNamingPolicy()
};
json = JsonSerializer.Serialize(products, options);
json
[
{
"ID": 2,
"PRDUCTNAME": "Harry Potter",
"CATEGORY": "Books",
"PRICE": 24.99,
"PURCHASEDDATE": "2024-09-24T10:20:30"
},
{
"ID": 3,
"PRDUCTNAME": "Console",
"CATEGORY": "Electronics",
"PRICE": 199.99,
"PURCHASEDDATE": "2024-09-25T12:22:45"
},
{
"ID": 4,
"PRDUCTNAME": "Pen",
"CATEGORY": null,
"PRICE": 10.0,
"PURCHASEDDATE": "2024-09-25T06:10:15"
},
{
"ID": 5,
"PRDUCTNAME": "TShirt",
"CATEGORY": "Clothing",
"PRICE": 49.99,
"PURCHASEDDATE": "2024-09-27T08:12:20"
},
{
"ID": 1,
"PRDUCTNAME": "Laptop",
"CATEGORY": "Electronics",
"PRICE": 299.99,
"PURCHASEDDATE": "2024-09-27T09:55:51.3731903+09:00"
}
]

Create a Custom Converter

A custom converter class is used to control how specific types are serialized and deserialized. You inherit from JsonConverter<T>, where T is the type you want to customize.

public class CustomDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.ParseExact(reader.GetString(), "yyyy-MM-dd", null);
}

public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
}
}

options = new JsonSerializerOptions
{
WriteIndented = true,
};
options.Converters.Add(new CustomDateTimeConverter()); // you can add multiple converters

json = JsonSerializer.Serialize(products, options);
json
[
{
"ID": 2,
"PrductName": "Harry Potter",
"Category": "Books",
"Price": 24.99,
"PurchasedDate": "2024-09-24"
},
{
"ID": 3,
"PrductName": "Console",
"Category": "Electronics",
"Price": 199.99,
"PurchasedDate": "2024-09-25"
},
{
"ID": 4,
"PrductName": "Pen",
"Category": null,
"Price": 10.0,
"PurchasedDate": "2024-09-25"
},
{
"ID": 5,
"PrductName": "TShirt",
"Category": "Clothing",
"Price": 49.99,
"PurchasedDate": "2024-09-27"
},
{
"ID": 1,
"PrductName": "Laptop",
"Category": "Electronics",
"Price": 299.99,
"PurchasedDate": "2024-09-27"
}
]

In this example the custom converter for DateTime that formats dates in yyyy-MM-dd format.

Handling Case Sensitivity

Handling case sensitivity in JSON serialization and deserialization is an important aspect when dealing with JSON data where property names might differ in casing. In C#, you can control case sensitivity using the JsonSerializerOptions.PropertyNameCaseInsensitive property.

Example 1: Case-Sensitive Deserialization (Default Behavior)

By default, System.Text.Json is case-sensitive, meaning that property names in the JSON must exactly match the property names in the C# class.

public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
// JSON with property names that don't match C# class case exactly
json = "{\"firstname\": \"Ron\", \"lastname\": \"Weasley\"}";

// Deserialize without setting case-insensitivity (default behavior is case-sensitive)
var person = JsonSerializer.Deserialize<Person>(json);
person
FirstName: <null>
LastName: <null>

Since the property names in the JSON (“firstname” and “lastname”) are all lowercase, and the Person class has properties with FirstName and LastName using PascalCase, deserialization failsto map the JSON properties to the class fields, leaving them as null.

Example 2: Enabling Case-Insensitive Deserialization

You can enable case-insensitive property name matching during deserialization by setting PropertyNameCaseInsensitive = true in JsonSerializerOptions. This makes the deserialization process ignore case differences between JSON property names and C# property names.

// Enable case-insensitive deserialization
options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
// Deserialize with case-insensitivity enabled
person = JsonSerializer.Deserialize<Person>(json, options);
person
FirstName: Ron
LastName: Weasley

In this case, even though the JSON uses lowercase for the property names,deserialization succeeds because case sensitivity has been disabled.

Handling Cyclic References

Example 1: Enabling Reference Handling

You can use the ReferenceHandler.Preserve option to enable reference handling in JSON serialization. This will handle circular references by using special $id and $ref properties in the JSON.

Consider two classes, Person and Address, where each can reference the other, creating a cyclic reference.

public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
}

public class Address
{
public string City { get; set; }
public Person Resident { get; set; } // Circular reference back to Person
}

var person = new Person { Name = "Ron Weasley" };
var address = new Address { City = "Hogwarts Gryffindor", Resident = person };
person.Address = address;

// Set up JsonSerializerOptions with ReferenceHandler.Preserve
options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve, // Enable reference handling
WriteIndented = true // Format the JSON for readability
};

// Serialize the object graph with cyclic references
json = JsonSerializer.Serialize(person, options);
json
{
"$id": "1",
"Name": "Ron Weasley",
"Address": {
"$id": "2",
"City": "Hogwarts Gryffindor",
"Resident": {
"$ref": "1"
}
}
}
var deserializedPerson = JsonSerializer.Deserialize<Person>(json, options);
deserializedPerson

The $id and $ref properties are used to manage cyclic references. In this case

  • *$id: "1" represents the Person object.
  • $ref: "1" means the Address object's Resident property refers back to the same Person object with $id: "1", resolving the circular reference.

This approach prevents infinite recursion and handles circular references gracefully.

Example 2: Ignoring Cyclic References

If you want to ignore cyclic references during serialization (i.e., not serialize the properties that would cause a cycle), you can use the ReferenceHandler.IgnoreCycles option.

// Set up JsonSerializerOptions with ReferenceHandler.IgnoreCycles
var options = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.IgnoreCycles, // Ignore circular references
WriteIndented = true // Format the JSON for readability
};

// Serialize the object graph ignoring cyclic references
json = JsonSerializer.Serialize(person, options);
json
{
"Name": "Ron Weasley",
"Address": {
"City": "Hogwarts Gryffindor",
"Resident": null
}
}
deserializedPerson = JsonSerializer.Deserialize<Person>(json, options);
deserializedPerson

Example 3: Handling Cyclic References with JsonIgnore

You can also manually prevent cyclic references by using the [JsonIgnore] attribute on properties that would cause a cycle.

public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
}

public class Address
{
public string City { get; set; }

[JsonIgnore] // Ignore the cyclic reference during serialization
public Person Resident { get; set; }
}


var person = new Person { Name = "Ron Weasley" };
var address = new Address { City = "Hogwarts Gryffindor", Resident = person };
person.Address = address;

// Serialize the object graph with the [JsonIgnore] attribute
json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
json
{
"Name": "Ron Weasley",
"Address": {
"City": "Hogwarts Gryffindor"
}
}
  • The [JsonIgnore] attribute prevents the Resident property from being serialized, thus avoiding the cyclic reference.
  • This is a simple way to manually control which properties should be excluded from the serialization process.

Check YouTube Video :

video demo

Conclusion

In summary, we have explored how to use System.Text.Json to serialize and deserialize objects in C#. We learned how to customize the JSON output using JsonSerializerOptions and handle null values, date formatting, and property naming conventions. Also we have explored how to use Handling Case Sensitivity & Handling Cyclic References.

--

--

Responses (1)