In this post, we are going to learn how to write a JSON into a file in C#.
JSON is structured, lightweight, widely supported, more readable, and more efficient than its XML counterpart. That’s why, many applications nowadays prefer JSON for storing structured data, especially metadata such as configurations, user preferences, and so on. .NET applications are no different from that. As such, we see .NET leaning to JSON-based configuration files like appSettings.json
, launchSettings.json
, etc.
We are going to explore different ways of writing JSON data to a file using the inbuilt System.Text.Json library and the popular Newtonsoft.Json
library.
Let’s get to work.
Preparation of Object Data Source
Let’s assume we want to write a survey result of neighborhood colleges in JSON format. We can start with some building blocks in our class library project.
The College
record:
// College.cs public record class College( string Name, int NoOfStudents, bool IsPublic);
And the SurveyReport
class:
public static class SurveyReport { public static List<College> GetColleges() => new() { new("Juvenile", 300, false), new("Cambrian", 650, true), new("Mapple Leaf", 450, true) }; }
Now, let’s continue.
Write JSON File: Simplistic Approach
We are going to add two utility classes, one for each target JSON library, to hold our JSON-writing routines:
// Native/JsonFileUtils.cs public static class JsonFileUtils { private static readonly JsonSerializerOptions _options = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; public static void SimpleWrite(object obj, string fileName) { var jsonString = JsonSerializer.Serialize(obj, _options); File.WriteAllText(fileName, jsonString); } } // Newtonsoft/JsonFileUtils.cs public static class JsonFileUtils { private static readonly JsonSerializerSettings _options = new() { NullValueHandling = NullValueHandling.Ignore }; public static void SimpleWrite(object obj, string fileName) { var jsonString = JsonConvert.SerializeObject(obj, _options); File.WriteAllText(fileName, jsonString); } }
Right at the top of each class, we configure a JsonSerializerOptions
options (JsonSerializerSettings
for Newtonsoft) for ignoring null values. We will use this _options
to produce a clean JSON output.
Straightaway, we go for the first quick implementation: SimpleWrite
. First, we serialize the object to a string using JsonSerializer.Serialize
method for the native version and JsonConvert.SerializeObject
for Newtonsoft. Then, we write this string to file using File.WriteAllText
. As simple as that.
After that, we can call this SimpleWrite
method with the college list:
var colleges = SurveyReport.GetColleges(); var fileName = "Colleges.json"; JsonFileUtils.SimpleWrite(colleges, fileName);
This creates a Colleges.json
file in the output directory that contains the resulting JSON:
[{"Name":"Juvenile","NoOfStudents":300,"IsPublic":false},{"Name":"Cambrian","NoOfStudents":650,"IsPublic":true},{"Name":"Mapple Leaf","NoOfStudents":450,"IsPublic":true}]
That’s the output we desire. But, this does not look readable, right?
Write JSON File with Better Readability
So, we want a better readable output file:
// Native/JsonFileUtils.cs public static void PrettyWrite(object obj, string fileName) { var options = new JsonSerializerOptions(_options) { WriteIndented = true }; var jsonString = JsonSerializer.Serialize(obj, options); File.WriteAllText(fileName, jsonString); } // Newtonsoft/JsonFileUtils.cs public static void PrettyWrite(object obj, string fileName) { var jsonString = JsonConvert.SerializeObject(obj, Formatting.Indented, _options); File.WriteAllText(fileName, jsonString); }
Here, we can see the pretty-print variant of our previous routine.
For the native version, we instantiate a JsonSerializerOptions
with its WriteIndented
flag enabled. Then we call the appropriate Serialize
overload. In contrast, Newtonsoft’s SerializeObject
directly accepts Formatting.Indented
option.
Once we use this PrettyWrite
method:
var colleges = SurveyReport.GetColleges(); var fileName = "Colleges-pretty.json"; JsonFileUtils.PrettyWrite(colleges, fileName);
We get a nicely formatted JSON file:
[ { "Name": "Juvenile", "NoOfStudents": 300, "IsPublic": false }, { "Name": "Cambrian", "NoOfStudents": 650, "IsPublic": true }, { "Name": "Mapple Leaf", "NoOfStudents": 450, "IsPublic": true } ]
Nice! The content is greatly readable now!
However, we should avoid pretty printing for the production code. The fact is it generates a lot of extra white spaces that affect the performance and the bandwidth. Apart from that, “readability” is purely a display capability that should not generally dictate “storage” responsibility. This should be the responsibility of the presenter program that we usually manage by a browser plugin or editor setting.
Write JSON File: Efficient Approach
We now have a basic JSON-writing routine which is inefficient for several reasons:
- Serializes to a string before writing to the file, causing performance cost for UTF-16 conversion
- Buffers the full output in a local copy causing substantial memory overhead
- Expensive I/O operation blocking the running thread
Let’s explore the options to handle these bottlenecks one by one.
Writing UTF-8 Bytes Instead of UTF-16 String
First and foremost, we want to avoid the expensive string (UTF-16) conversion. We can serialize to UTF-8 bytes instead, which is about 5-10% faster than using the string-based methods. The difference is because we don’t need to convert bytes (as UTF-8) to strings (UTF-16):
// Native/JsonFileUtils.cs public static void Utf8BytesWrite(object obj, string fileName) { var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(obj, _options); File.WriteAllBytes(fileName, utf8Bytes); }
We come up with the Utf8BytesWrite
method in two steps: get serialized output directly in bytes and write the file accordingly.
With the native library, this is just a two-liner code. It provides the JsonSerializer.SerializeToUtf8Bytes
method to get byte output directly.
Newtonsoft, on the other hand, does not offer any direct way to do this. But, we can implement our own version of the SerializeToUtf8Bytes
method:
// Newtonsoft/JsonFileUtils.cs static byte[] SerializeToUtf8Bytes(object obj, JsonSerializerSettings options) { using var stream = new MemoryStream(); using var streamWriter = new StreamWriter(stream); using var jsonWriter = new JsonTextWriter(streamWriter); JsonSerializer.CreateDefault(options).Serialize(jsonWriter, obj); jsonWriter.Flush(); stream.Position = 0; return stream.ToArray(); }
We start with a MemoryStream
instance (stream
) just like a typical byte-output routine does.
What comes next is to find a stream-supported serializer routine. Unlike our previous examples, this time we don’t get help from JsonConvert
. Instead, JsonSerializer
class is the rescuer here. This offers a Serialize
method that works on a StreamWriter/TextWriter
or a JsonTextWriter
object. So, we create a StreamWriter
from the stream
, wrap it within a JsonTextWriter
instance and call the Serialize
method accordingly. We can directly use the StreamWriter
instance but the JsonTextWriter
makes it efficient.
Subsequently, we ensure the completion of writing by calling Flush
and then reset the stream position to get the full byte array. That’s it.
We can now implement the Newtonsoft version of the Utf8BytesWrite
method just like the native version:
// Newtonsoft/JsonFileUtils.cs public static void Utf8BytesWrite(object obj, string fileName) { var utf8Bytes = SerializeToUtf8Bytes(obj, _options); File.WriteAllBytes(fileName, utf8Bytes); }
Writing JSON with this Utf8BytesWrite
method generates the output faster and we will see that in our performance section of this article.
That said, we have an even better way to deal with this.
Write Directly to FileStream
We don’t want to eat up the memory holding the full output in a local copy. It can be disastrous if we have a large object graph. We don’t need that actually. Both libraries offer a serializing method that writes directly to the file stream:
// Native/JsonFileUtils.cs public static void StreamWrite(object obj, string fileName) { using var fileStream = File.Create(fileName); using var utf8JsonWriter = new Utf8JsonWriter(fileStream); JsonSerializer.Serialize(utf8JsonWriter, obj, _options); } // Newtonsoft/JsonFileUtils.cs public static void StreamWrite(object obj, string fileName) { using var streamWriter = File.CreateText(fileName); using var jsonWriter = new JsonTextWriter(streamWriter); JsonSerializer.CreateDefault(_options).Serialize(jsonWriter, obj); }
StreamWrite
is the method that we create to directly write to a file stream.
Talking about the native version, we create a FileStream
using File.Create
method, wrap it in an Utf8JsonWriter
instance and call the appropriate Serialize
method of JsonSerializer
. We can also directly use the FileStream object, but the Utf8JsonWriter
provides the most efficient operation.
For Newtonsoft, we already know how to work with a stream-writer. This time we don’t need to handle the raw stream directly. We create a StreamWriter
using File.CreateText
, wrap it within a JsonTextWriter
instance, and call the Serialize
method like before.
The best thing about this StreamWrite
version is, this solves our first two problems simultaneously.
Asynchronous Writing
We’re not done yet. As mentioned before, we should not block the running thread keeping it waiting for an expensive I/O operation to complete. While this does not matter for the performance of the JSON-write operation itself, it does matter for the responsiveness and/or scalability of the caller program. So, we need an asynchronous version:
public static async Task StreamWriteAsync(object obj, string fileName) { await using var fileStream = File.Create(fileName); await JsonSerializer.SerializeAsync(fileStream, obj, _options); }
This is very similar to the synchronous StreamWrite
version. Thanks to the native JsonSerializer.SerializeAsync
method that makes it possible. The rest part is typical async method semantics. FileStream
construction is awaitable. So, in our case, both lines can be marked await
. Finally, we adjust the method signature as an async Task
. That’s all.
Unfortunately, NewtonsoftJson doesn’t support asynchronous serialization out of the box yet. What we can do at best is offload this I/O operation to thread-pool:
public static async Task StreamWriteAsync(object obj, string fileName) { await Task.Run(() => StreamWrite(obj, fileName)); }
We simply wrap the synchronous version in Task.Run
construct and await
the returned task instance. This is not a proper asynchronous operation and does not scale up in terms of thread consumption. But in our case, this at least serves the purpose of non-blocking I/O operation.
Write Mutable JSON Object to File
So far, we’ve seen how to write JSON from POCO objects. What about writing a mutable JSON object e.g. JsonObject
(native) or JObject
(Newtonsoft)?
They’re useful means of making up dynamic JSON data on demand. A typical use-case is: we wrap raw JSON string coming from an external source in a mutable JSON object, customize as we need, and then generate modified JSON output. The good thing is, these classes provide a WriteTo
method that supports direct stream-writing:
// Native/JsonFileUtils.cs public static void WriteDynamicJsonObject(JsonObject jsonObj, string fileName) { using var fileStream = File.Create(fileName); using var utf8JsonWriter = new Utf8JsonWriter(fileStream); jsonObj.WriteTo(utf8JsonWriter); } // Newtonsoft/JsonFileUtils.cs public static void WriteDynamicJsonObject(JObject jsonObj, string fileName) { using var streamWriter = File.CreateText(fileName); using var jsonWriter = new JsonTextWriter(streamWriter); jsonObj.WriteTo(jsonWriter); }
Once again, we build a helper routine in a few simple steps. Inside the method, we create the relevant JSON writer as usual and invoke the WriteTo
method with the writer instance. That’s it.
When we try this method on a dynamic JSON object:
// NativeJsonFileUtilsUnitTest.cs var fileName = "Dynamic-json.json"; var jsonObj = new JsonObject { ["Name"] = "SunnyDale", ["NoOfStudents"] = 200, ["IsPublic"] = true }; JsonFileUtils.WriteDynamicJsonObject(jsonObj, fileName); // NewtonsoftJsonFileUtilsUnitTest.cs var fileName = "Dynamic-json.json"; var jsonObj = new JObject { ["Name"] = "SunnyDale", ["NoOfStudents"] = 200, ["IsPublic"] = true }; JsonFileUtils.WriteDynamicJsonObject(jsonObj, fileName);
We get a JSON file as expected:
{"Name":"SunnyDale","NoOfStudents":200,"IsPublic":true}
Performance Comparison of Different Variants
We now have different variants of JSON-writing routines. It’s time for benchmarking these methods.
Prepare Complex Object Graph
So far, we work with a very simple data structure that can merely produce any difference in performance. Let’s introduce some layers of nesting there:
// College.cs public record class College( string Name, int NoOfStudents, bool IsPublic, List<Teacher>? Teachers = null); public record class Teacher( string Name, int WorkHours, bool InProbation, List<Course>? Courses = null); public record class Course( string Name, int CreditHours, bool IsOptional);
First, we modify the College
record to include a property for List<Teacher>
. Teacher
is the 2nd tier of data in our object graph. This model contains three basic properties and property for List<Course>
. Course
is the 3rd data-tier in the graph having only three basic properties.
Next, we enrich our SurveyReport
class with another data-provider method:
public static IEnumerable<College> GetColleges( int noOfColleges, int teachersPerCollege, int coursesPerTeacher) { for (var i = 0; i < noOfColleges; i++) { yield return new($"College{i}", 100, true, GetTeachers()); } List<Teacher>? GetTeachers() => Enumerable.Repeat<Teacher>(new($"John", 8, false, GetCourses()), teachersPerCollege).ToList(); List<Course>? GetCourses() => Enumerable.Repeat<Course>(new($"Course", 4, false), coursesPerTeacher).ToList(); }
This method generates data for a batch of colleges including teachers and their assigned courses.
Benchmark Analysis
Once we run the benchmark on a dataset of 500 colleges, 25 teachers per college, and 3 courses per teacher (nearly 50K records in total), we can inspect our result:
| Method | Mean | Error | StdDev | Ratio | RatioSD | |------------------------- |---------:|---------:|---------:|------:|--------:| | StreamWrite | 845.1 ms | 3.81 ms | 3.38 ms | 0.98 | 0.01 | | Utf8BytesWrite | 849.7 ms | 6.10 ms | 5.40 ms | 0.99 | 0.01 | | SimpleWrite | 861.6 ms | 6.76 ms | 5.64 ms | 1.00 | 0.00 | | PrettyWrite | 921.6 ms | 13.92 ms | 13.02 ms | 1.07 | 0.01 | | | | | | | | | NewtonsoftStreamWrite | 869.3 ms | 9.20 ms | 7.68 ms | 0.98 | 0.01 | | NewtonsoftUtf8BytesWrite | 871.8 ms | 10.89 ms | 10.19 ms | 0.98 | 0.00 | | NewtonsoftSimpleWrite | 883.7 ms | 17.32 ms | 17.01 ms | 1.00 | 0.03 | | NewtonsoftPrettyWrite | 970.4 ms | 17.06 ms | 15.12 ms | 1.10 | 0.01 |
Our two key findings here: direct stream writing (StreamWrite
) is the fastest process and System.Text.Json
outperforms Newtonsoft.Json
in all cases.
We can anticipate that these performance gaps grow higher with the larger dataset.
Overall, we should go with the direct file-writing method and avoid pretty writing in general.
Conclusion
In this article, we have learned a few ways to write a JSON file in C#. We have also discussed some performance factors along with benchmarking analysis.