Mastering JSON Serialization in C# with System.Text.Json
Customize and Control JSON Output with JsonSerializerOptions
for Cleaner, Efficient Data Handling
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 thePerson
object.$ref: "1"
means theAddress
object'sResident
property refers back to the samePerson
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 theResident
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 :
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.