Updated on

To turn a C# object into a JSON string, we call JsonSerializer.Serialize(obj) from System.Text.Json, which ships with .NET, or JsonConvert.SerializeObject(obj) from the Newtonsoft Json.NET package. Both return the JSON as a plain string.

We’ll use the two main JSON libraries in .NET side by side, and then control the output: indentation, property name casing, skipped properties, dates and reference loops. Going the other way, from a JSON string to an object, is deserialization, and we cover it in deserializing JSON into a C# class. If we only need to know whether a string holds JSON at all, we have a guide on checking whether a string is valid JSON.

To download the source code for the video, visit our Patreon page (YouTube Patron tier).

Let’s start.


VIDEO: Convert a C# Object to JSON Using System.Text.JSON and Newtonsont.JSON.


How Do We Serialize an Object to JSON With System.Text.Json?

We pass the object to JsonSerializer.Serialize() and get the JSON string back. The method lives in the System.Text.Json namespace, and we don’t need a NuGet package for it, because System.Text.Json has been part of .NET since .NET Core 3.0. It is Microsoft’s own serializer, and ASP.NET Core uses it out of the box.

With no options, the serializer writes every public property that has a public getter. Names stay exactly as we declared them, the whole object goes on one line, and properties holding null are written too.

Fields and private properties are left out unless we ask for them.

Each of those defaults has a setting we can change, and the sections below go through them one by one. For every setting, we show the System.Text.Json version first and the Newtonsoft version right after it, so we can compare the two libraries directly.

Our Product class has three public properties: string Name, int? Stock and DateTime? DateAcquired. Here is the call on a Product object:

var obj = new Product
{
    Name = "Red Apples",
    Stock = 100,
    DateAcquired = DateTime.Parse("2017-08-24")
};

var jsonString = JsonSerializer.Serialize(obj);

Here, we see how we can use the Serialize() static method of the JsonSerializer to get the JSON string that results from an object passed as a parameter:

{"Name":"Red Apples","Stock":100,"DateAcquired":"2017-08-24T00:00:00"}

Using Newtonsoft Json.NET to Serialize C# Objects

Before System.Text.Json, the usual choice was Newtonsoft Json.NET, a NuGet package that was also the default JSON serializer in ASP.NET Core up to version 2.2. Plenty of projects still use it. We add it with dotnet add package Newtonsoft.Json and a using Newtonsoft.Json; line, and the call is just as short:

var jsonString = JsonConvert.SerializeObject(obj);

Here, we turn an object into a JSON string by calling the SerializeObject() static method of the JsonConvert class. Newtonsoft has its own JsonSerializer class too, so in a file that imports both namespaces we add using JsonSerializer = System.Text.Json.JsonSerializer; to keep the System.Text.Json calls compiling.

How to Generate Pretty JSON Strings

Often, we’ll rather have indented or “pretty” JSON strings. We can easily achieve that with System.Text.Json by using the WriteIndented property of the JsonSerializerOptions object:

var options = new JsonSerializerOptions
{
    WriteIndented = true
};

var jsonString = JsonSerializer.Serialize(obj, options);

In a similar fashion, Newtonsoft has it even easier. We can include the Formatting.Indented enumerated value directly in the call to the SerializeObject() method:

var jsonString = JsonConvert.SerializeObject(obj, Formatting.Indented);

In both cases we will obtain a properly formatted JSON string: 

{
  "Name": "Red Apples",
  "Stock": 100,
  "DateAcquired": "2017-08-24T00:00:00"
}

Camel Case Property Names

Despite being a common formatting option, none of these libraries will format property names using camel case by default. ASP.NET Core is the exception: its web defaults switch to camel case, and since .NET 9 the same settings are available as JsonSerializerOptions.Web. We explain those defaults in our article on camel case serialization with System.Text.Json. With System.Text.Json, we need to explicitly set the PropertyNamingPolicy option in our JsonSerializerOptions object:

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

var jsonString = JsonSerializer.Serialize(obj, options);

Similarly, with Newtonsoft we need to populate the ContractResolver property in our JsonSerializerSettings with an instance of the CamelCasePropertyNamesContractResolver class:

using Newtonsoft.Json.Serialization;

var options = new JsonSerializerSettings
{
    Formatting = Formatting.Indented,
    ContractResolver = new CamelCasePropertyNamesContractResolver()
};

var jsonString = JsonConvert.SerializeObject(obj, options);

When used this way, both libraries will generate camel case JSON properties when serializing objects:

{
  "name": "Red Apples",
  "stock": 100,
  "dateAcquired": "2017-08-24T00:00:00"
}

How to Ignore Certain Properties When Serializing Objects

Frequently, we’ll want to prevent some of our object’s properties from showing up in the resulting JSON string.

Ignoring NULL Values in Serialization

One specific scenario is when we do not want to include the properties that contain null values. We can use JsonSerializerOptions object in System.Text.Json to set the DefaultIgnoreCondition property:

using System.Text.Json.Serialization;

var obj = new Product
{
    Name = "Red Apples",
    Stock = null,
    DateAcquired = null
};

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

var jsonString = JsonSerializer.Serialize(obj, options);

If we are using Newtonsoft, we’ll use the NullValueHandling property in JsonSerializerSettings:

var options = new JsonSerializerSettings
{
    Formatting = Formatting.Indented,
    NullValueHandling = NullValueHandling.Ignore
};

var jsonString = JsonConvert.SerializeObject(obj, options);

Console.WriteLine(jsonString);

The resulting JSON string will not include any of the null properties:

{
  "Name": "Red Apples"
}

Ignoring Specific Properties

These libraries include attributes that will let us provide serialization options on a per property basis. The most basic example of this is the JsonIgnore attribute that exists in both System.Text.Json and Newtonsoft.

We can use the attribute located in the System.Text.Json.Serialization namespace on one or more properties in our object:

public class Customer
{
    public string Name { get; set; }

    public string Address { get; set; }

    [JsonIgnore]
    public decimal FinancialRating { get; set; }
}

Consequently, the resulting JSON string will not contain the FinancialRating property even though it was present in the original object:

{
  "Name": "Pumpkins And Roses",
  "Address": "158 Barfield Rd"
}

If we are using Newtonsoft, we can achieve the same result by using the JsonIgnore attribute located in the Newtonsoft.Json namespace.

In System.Text.Json, [JsonIgnore] can also take a condition, so a property is skipped only when it holds null or its default value. We cover those variations in our guide to excluding properties from JSON serialization.

How to Serialize Anonymous and Dynamic Objects

Both libraries can handle anonymous objects correctly, out of the box. Here, optionsSystem and optionsNewtonsoft are the indented options from earlier, new JsonSerializerOptions { WriteIndented = true } and new JsonSerializerSettings { Formatting = Formatting.Indented }.

var obj = new 
{ 
    FirstName = "John", 
    LastName = "Smith" 
};

// System.Text.Json
var jsonStringSystem = JsonSerializer.Serialize(obj, optionsSystem);

// Newtonsoft
var jsonStringNewtonsoft = JsonConvert.SerializeObject(obj, optionsNewtonsoft);
{
  "FirstName": "John",
  "LastName": "Smith"
}

Similarly, dynamic objects work fine as well with both System.Text.Json and Newtonsoft:

using System.Dynamic;

dynamic dynObj = new ExpandoObject();
dynObj.Name = "Corey Richards";
dynObj.Address = "3519 Woodburn Rd";

// System.Text.Json
var jsonStringSystem = JsonSerializer.Serialize(dynObj, optionsSystem);

// Newtonsoft
var jsonStringNewtonsoft = JsonConvert.SerializeObject(dynObj, optionsNewtonsoft);
{
  "Name": "Corey Richards",
  "Address": "3519 Woodburn Rd"
}

How to Control Date and Time Format

System.Text.Json will use ISO-8601-1:2019 format when serializing DateTime or DateTimeOffset properties in our objects, like in this example: 2017-08-24T16:59:57-02:00. However, we can customize that by creating a custom converter:

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

public class GeneralDateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateTime.ParseExact(reader.GetString() ?? string.Empty, "G", new CultureInfo("en-US"));
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("G", new CultureInfo("en-US")));
    }
}

By using this converter in our serialization code, we will get DateTime properties in the general date long time format, which prints our date as 8/24/2017 12:00:00 AM on our Windows 10 test machine. On Linux, recent ICU data puts a narrow no-break space before AM, so the string isn’t byte for byte the same on every machine:

var options = new JsonSerializerOptions();
options.Converters.Add(new GeneralDateTimeConverter());

var obj = new
{
    DateCreated = new DateTime(2017, 8, 24)
};

var jsonString = JsonSerializer.Serialize(obj, options); // {"DateCreated":"8/24/2017 12:00:00 AM"}

Alternatively, we can apply our custom converter only to specific properties:

[JsonConverter(typeof(GeneralDateTimeConverter))]
public DateTime BirthDate { get; set; }

In Newtonsoft, using a custom date and time format is done in a very similar way. We can create our custom converter that inherits from the Newtonsoft.Json.JsonConverter abstract class:

using System.Globalization;
using Newtonsoft.Json;

public class GeneralDateTimeNewtonsoftConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(DateTime);
    }

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        var dataString = reader.Value as string;
        return DateTime.ParseExact(dataString ?? string.Empty, "G", new CultureInfo("en-US"));
    }

    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        var dateTimeValue = value as DateTime?;
        writer.WriteValue(dateTimeValue?.ToString("G", new CultureInfo("en-US")));
    }
}

Once we have our custom converter we can pass it directly to the serialization call:

var jsonString = JsonConvert.SerializeObject(obj, new GeneralDateTimeNewtonsoftConverter());

Or by decorating our object’s properties with the Newtonsoft.Json.JsonConverterAttribute.

Reference Loops

To understand what a reference loop is, consider the following C# models:

public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Department Department { get; set; }
}

public class Department
{
    public string? Name { get; set; }
    public IList<Employee> Staff { get; set; } = new List<Employee>();
}

Here, we can see how the Employee class references the Department class that, in turn, references back to the Employee class through its Staff property.

When trying to turn a circular data structure into a JSON string, we will get an error no matter if we are using System.Text.Json:

JsonException: A possible object cycle was detected.

Or Newtonsoft:

JsonSerializationException: Self referencing loop detected with type 'Employee'. Path 'Department.Staff'.

Options for Handling Reference Loops

The best way to avoid this situation is to design our models so they do not contain reference loops. However, if that’s not possible, JSON libraries offer options to help us deal with circular references.

For System.Text.Json, we can set the JsonSerializerOptions.ReferenceHandler property to one of the built-in ReferenceHandler values:

var employee = new Employee { FirstName = "John", LastName = "Smith" };
var department = new Department { Name = "Human Resources" };
employee.Department = department;
department.Staff.Add(employee);

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    ReferenceHandler = ReferenceHandler.Preserve
};

var jsonString = JsonSerializer.Serialize(department, options);

Here, we set the ReferenceHandler property to ReferenceHandler.Preserve. With this, we instruct the serializer to preserve the object graph structure by including metadata in the serialized objects, and resolve reference loops using pointers based on generated unique object identifiers:

{
  "$id": "1",
  "Name": "Human Resources",
  "Staff": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "FirstName": "John",
        "LastName": "Smith",
        "Department": {
          "$ref": "1"
        }
      }
    ]
  }
}

Alternatively, we can use the ReferenceHandler.IgnoreCycles value to tell the serializer to simply ignore circular references:

{
  "Name": "Human Resources",
  "Staff": [
    {
      "FirstName": "John",
      "LastName": "Smith",
      "Department": null
    }
  ]
}

Newtonsoft has the same two choices. To keep the whole graph, we set PreserveReferencesHandling in our JsonSerializerSettings:

var options = new JsonSerializerSettings
{
    Formatting = Formatting.Indented,
    PreserveReferencesHandling = PreserveReferencesHandling.Objects
};

var jsonString = JsonConvert.SerializeObject(department, options);

Newtonsoft writes the same $id and $ref metadata, but it leaves the list as a plain array instead of wrapping it in $values:

{
  "$id": "1",
  "Name": "Human Resources",
  "Staff": [
    {
      "$id": "2",
      "FirstName": "John",
      "LastName": "Smith",
      "Department": {
        "$ref": "1"
      }
    }
  ]
}

To drop the back-reference instead, we use the ReferenceLoopHandling property:

var options = new JsonSerializerSettings
{
    Formatting = Formatting.Indented,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};

Where IgnoreCycles wrote "Department": null, Newtonsoft leaves the property out completely:

{
  "Name": "Human Resources",
  "Staff": [
    {
      "FirstName": "John",
      "LastName": "Smith"
    }
  ]
}

We go deeper into this in our article on handling circular references in ASP.NET Core.

How Do We Reuse Options and Use Source Generation?

System.Text.Json learns the shape of our types at run time, through reflection. The first time it meets Product, it inspects the properties, builds the serialization metadata, and caches that metadata on the JsonSerializerOptions instance we passed in.

That cache is why the options object matters. If we create a new JsonSerializerOptions inside every call, every new instance has to find or rebuild that cache, and the reflection work can run again. So we create the options once, keep them in a static readonly field, and reuse them everywhere.

Source generation goes one step further. We declare a partial class that derives from JsonSerializerContext and list our types with [JsonSerializable]. The compiler then writes the metadata during the build, so nothing has to be discovered at run time, which is what Native AOT and trimmed apps need.

The JSON itself doesn’t change. Our test serializes the same Product both ways and gets identical strings.

Microsoft Learn puts the reuse rule plainly: “If you use JsonSerializerOptions repeatedly with the same options, don’t create a new JsonSerializerOptions instance each time you use it.” The guidance is on its page about instantiating JsonSerializerOptions.

Let’s start with the reused options. We declare them once, as a field in the class that does the serializing:

private static readonly JsonSerializerOptions IndentedOptions = new()
{
    WriteIndented = true
};

Then every call passes the same instance:

var jsonString = JsonSerializer.Serialize(obj, IndentedOptions);

For source generation, we add a context class to the project:

using System.Text.Json.Serialization;

[JsonSerializable(typeof(Product))]
public partial class ProductJsonContext : JsonSerializerContext
{
}

The source generator fills in the rest of the class, including a Default instance with a property for every type we listed. We pass that property instead of an options object:

var jsonString = JsonSerializer.Serialize(obj, ProductJsonContext.Default.Product);

The result is the same compact string as in the first section:

{"Name":"Red Apples","Stock":100,"DateAcquired":"2017-08-24T00:00:00"}

Most console apps don’t need a context class. Reflection works fine there, and reusing the options instance is the change that matters everywhere. The context class earns its keep when we publish with Native AOT or trimming, or when we want the serializer’s type metadata decided at build time.

How Do the Two Libraries’ Settings Compare?

Almost every setting in this article exists in both libraries, mostly under a different name, so here they are side by side:

What we wantSystem.Text.JsonNewtonsoft Json.NET
Serialize an objectJsonSerializer.Serialize(obj)JsonConvert.SerializeObject(obj)
Options objectJsonSerializerOptionsJsonSerializerSettings
Indented outputWriteIndented = trueFormatting = Formatting.Indented
camelCase namesPropertyNamingPolicy = JsonNamingPolicy.CamelCaseContractResolver = new CamelCasePropertyNamesContractResolver()
Skip null valuesDefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNullNullValueHandling = NullValueHandling.Ignore
Skip one property[JsonIgnore] from System.Text.Json.Serialization[JsonIgnore] from Newtonsoft.Json
Custom date formata JsonConverter<DateTime> subclassa JsonConverter subclass
Reference loop, keep the graphReferenceHandler = ReferenceHandler.PreservePreserveReferencesHandling = PreserveReferencesHandling.Objects
Reference loop, drop the back-referenceReferenceHandler = ReferenceHandler.IgnoreCycles (writes null)ReferenceLoopHandling = ReferenceLoopHandling.Ignore (leaves the property out)
Compile-time metadataa JsonSerializerContext with [JsonSerializable]no built-in equivalent

Two rows behave differently even though they look alike. When we keep the graph, System.Text.Json wraps the list in $values and Newtonsoft doesn’t. When we drop a back-reference, System.Text.Json writes the property as null and Newtonsoft leaves it out of the JSON. And only System.Text.Json can move its type metadata to compile time.

So which one should we pick? For new code, System.Text.Json is already in the box and needs no package. Newtonsoft is still a fine choice when a project already depends on it, or when we rely on one of its features that System.Text.Json handles differently. Switching a large codebase over only to swap serializers is rarely worth the effort.

Conclusion

In this article, we have learned how to turn a C# object into a JSON string. For that, we learned how to use System.Text.Json and Newtonsoft Json.NET as well.

We’ve learned how to customize various aspects of the serialization process like indentation, property capitalization, how to ignore specific properties, or how to change the date and time format. We also learned what reference loops are and how to deal with them, and how to reuse options and generate serialization code at compile time.

Finally, remember that we can find many other examples of how to work with System.Text.Json on this website.

Tested with .NET 10 (SDK 10.0.302, runtime 10.0.10) and Newtonsoft.Json 13.0.4.