Make a deep copy of a C# object instance with JSON serialization
In this post, I demonstrate reference types and how to make a deep copy of a reference type using JSON serialization.
In this post, I will discuss a method for converting a list of C# objects to a CSV file. This approach can be used to implement export to CSV functionality.
Converting a C# object into another format is called serialization. The .NET base class library includes the System.Text.Json
namespace. It provides classes that make it easy to work with JSON files, including serialization and deserialization. Deserialization is the opposite of serialization, which converts a JSON object into a .NET object. In this article, we focus on serialization.
Our demo project will create a CSV of the top-paying technology jobs based on this article on indeed.com: The Top 25 Highest-Paying Technology Jobs and Their Duties. We will only model our data for the top three highest-paying tech jobs.
Here is our Job
class:
public class Job
{
public string Title { get; set; } = string.Empty;
public decimal Salary { get; set; }
public string Description { get; set; } = string.Empty;
}
Next we will create a list of jobs.
var topThreeJobs = new List<Job>()
{
new()
{
Title = "Data Scientist",
Description = "A data scientist is responsible for collecting, cleaning and munging data to meet a company's purpose.",
Salary = 140722
},
new()
{
Title = "Enterprise Architect",
Description = "Enterprise architects are responsible for integrating an organization's information applications and programs.",
Salary = 137980
},
new()
{
Title = "Software Architect",
Description = "Software architects are software development professionals who oversee the technology infrastructure of a company.",
Salary = 133358
}
};
Now that we have our demo data, it’s time to serialize our topThreeJobs
object. We can use the built-in System.Text.Json
JsonSerializer
class to accomplish this task.
using System.Text.Json;
var options = new JsonSerializerOptions(){WriteIndented = true}
var serializedTopThreeJobs = JsonSerializer.Serialize(topThreeJobs, options);
I passed the optional options parameter to make the printed JSON more readable. If we write the serializedTopThreeJobs
variable to the console, you will see that we have JSON!
Console.WriteLine(serializedTopThreeJobs)
[
{
"Title": "Data Scientist",
"Salary": 140722,
"Description": "A data scientist is responsible for collecting, cleaning and munging data to meet a company\u0027s purpose."
},
{
"Title": "Enterprise Architect",
"Salary": 137980,
"Description": "Enterprise architects are responsible for integrating an organization\u0027s information applications and programs."
},
{
"Title": "Software Architect",
"Salary": 133358,
"Description": "Software architects are software development professionals who oversee the technology infrastructure of a company."
}
]
Now, we need to convert our new JSON data to a CSV. Since we have a JSON array, we must find a way to iterate the JSON array and write the list of property values as a row in our CSV file. We can use JsonSerializer
for this too. However, Instead of calling the Serialize
method on the JsonSerializer
class, let’s call the SerializeToDocument
, which will convert our C# object to a JsonDocument
, which will allow us to examine the JSON programmatically. Additionally, SerializerToDocument
implements IDisposable
, so we assign our variable with using
a declaration.
using var document = JsonSerializer.SerializeToDocument(topThreeJobs);
A JsonDocument is composed of JsonElements and each JsonDocument will have a RootElement
.
Let’s assign the root element to a variable;
JsonElement[] root = document.RootElement.EnumerateArray();
Next, we initialize a new instance of StringBuilder
to build our CSV string;
var builder = new StringBuilder();
We need the properties of each Job
so that we can write those properties as a row in the CSV file. But before we do that, we need to get the column headers. For this, we will iterate through the first object’s properties and add the the JsonProperty.Name
value to our builder object.
if (root.Any())
{
var headers = root.First().EnumerateObject().Select(o => o.Name);
builder.AppendJoin(',', headers);
}
Next, we can print builder object to the console to check our headers.
Console.WriteLine(builder);
Title,Salary,Description
Alright, that looks good! Now we can add the rows to our builder object.
if (root.Any())
{
var headers = root.First().EnumerateObject().Select(o => o.Name);
builder.AppendJoin(',', headers);
// new
foreach (var element in root)
{
var row = element.EnumerateObject().Select(o => o.Value.ToString());
builder.AppendJoin(',', row);
builder.AppendLine();
}
}
We used the EnumerateObject
method to iterate through the JsonProperty
object and add JsonProperty.Value
our builder object. Finally, we write the builder string to a file.
File.WriteAllText("jobs.csv", builder.ToString(), new UTF8Encoding());
After opening jobs.csv
, you will notice that we have a formatting issues.
Some of the description text in the first row was added to a new column. The program I loaded the CSV file into reads the comma in any text as the start of a new column. We could write a utility method to escape the commas before appending a new value to the builder object. A better way is to use the tools that Microsoft.Text.Json
provides to handle and implement any custom logic to convert JSON values.
I will create a custom converter to handle escaping the commas.
public class JsonConverterEscapeCommas : JsonConverter<string?>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
if (value is not null && value.Contains(','))
{
writer.WriteStringValue($"\"{value}\"");
}
else
{
writer.WriteStringValue(value);
}
}
}
In the preceding code, I created a custom converted called JsonConverterEscapeCommas
which is derived from JsonConverter<string?>
and overrides the Read
and Write
methods. We only need to modify the Write
method since this post is focused on serialization.
In the Write
method body, we do a check to see if the value is not null and contains a comma. If it does, we surround the value with double quotes to adhere to the RFC 480 Standard.
Next, we have to implement our custom converter. There are a couple ways to do this. We can add the custom converter to the JsonSerializerOptions
like so:
var options = new JsonSerializerOptions()
{
//...
Converters =
{
new JsonConverterEscapeCommas()
}
};
However, this would not be ideal for our use case because every string value would be passed through the custom converter. We only want to apply this convert to the Description
property of the Job
class. We can do this by simply decorating the property on the model with the JsonConverterAttribute
.
public class Job
{
public string Title { get; set; } = string.Empty;
public decimal Salary { get; set; }
[JsonConverter(typeof(JsonConverterEscapeCommas))]
public string Description { get; set; } = string.Empty;
}
Let’s test out the custom converter to see if the formatting issues in the CSV are resolved. After running the application, a new CSV file is created, and it looks great! 🎉
The System.Text.Json
namespace, included in the .NET base class library, is very useful, and this blog post barely scratches the surface of its capabilities. The demo provided in this post highlights how you can easily serialize objects and even control how the JsonSerializer
writes values, opening up many possibilities when customizing how values are converted to JSON.