System.Text.Json – How to serialize non-public properties

By default, System.Text.Json.JsonSerializer only serializes public properties. If you want to serialize non-public properties, you have two options:

In this article, I’ll show examples of both approaches for handling non-public properties.

Updated 2022-02-22 to explain the new JsonInclude attribute added in .NET 5.

Write a custom JSON converter to serialize non-public properties

When the built-in System.Text.Json functionality doesn’t fully support what you’re trying to do, you can write a custom JSON converter. In this case, when you want to (de)serialize non-public properties, you can write a custom JSON converter to (de)serialize all of the properties you want – including non-public ones.

Here’s an example of a custom JSON converter that gets public and non-public properties during serialization:

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

public class CustomPersonConverter : JsonConverter<Person>
{

	public override void Write(Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
	{
		writer.WriteStartObject();

		foreach (var prop in person.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
		{
			writer.WriteString(prop.Name, prop.GetValue(person)?.ToString());
		}
		writer.WriteEndObject();
	}
	public override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		//Intentionally not implemented
		throw new NotImplementedException();
	}
}
Code language: C# (cs)

Use the custom JSON converter by adding it to JsonSerializerOptions.Converters and passing in JsonSerializerOptions when serializing, like this:

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

var json = JsonSerializer.Serialize(person, options);
Code language: C# (cs)

In this article, I’ll show a full example of serializing and deserializing non-public properties.

Example of a custom JSON converter that serializes and deserializes non-public properties

Let’s say we want to serialize and deserialize all properties, including non-public properties. For this example, we’ll use objects of the following type (SystemEvent):

public class SystemEvent
{
	public string Name { get; set; }
	internal DateTimeOffset HappenedAt { get; set; }

	public SystemEvent()
	{
		HappenedAt = DateTimeOffset.Now;
	}
}
Code language: C# (cs)

Note: This is assuming the internal property can’t simply be changed to a public property. If you can do that in your situation, you probably wouldn’t be reading this.

Custom JSON converter

The following custom JSON converter uses reflection to get all of the properties, public and non-public.

  • In the constructor, it’s using reflection to lookup all the properties. It adds these to a dictionary to save on lookup costs during deserialization
  • Write() loops through the dictionary containing the property type info. It gets the value for each property from the object and serializes the value with Utf8JsonWriter.
  • Read() loops through the JSON properties with Utf8JsonReader. It gets the value from the dictionary using the JSON property name as the key. It reads the JSON property value and then sets the property value on the object.
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

public class CustomSystemEventConverter : JsonConverter<SystemEvent>
{
	private readonly Dictionary<string, PropertyInfo> PropertyMap;
	public CustomSystemEventConverter()
	{
		//Case-insensitive property names because JSON properties may be differently cased than the property names
		PropertyMap = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);

		foreach(var property in typeof(SystemEvent).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
		{
			PropertyMap.Add(property.Name, property);
		}
	}
	public override void Write(Utf8JsonWriter writer, SystemEvent systemEvent, JsonSerializerOptions options)
	{
		writer.WriteStartObject();

		foreach(var prop in PropertyMap.Values)
		{
			if (prop.PropertyType == typeof(string))
			{
				writer.WriteString(prop.Name, prop.GetValue(systemEvent)?.ToString());
			}
			else if (prop.PropertyType == typeof(DateTimeOffset))
			{
				writer.WriteString(prop.Name, ((DateTimeOffset)prop.GetValue(systemEvent)).ToString("o"));
			}
		}
		writer.WriteEndObject();
	}
	public override SystemEvent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		if (reader.TokenType != JsonTokenType.StartObject)
			throw new JsonException("Expected StartObject token");

		var systemEvent = new SystemEvent();

		while (reader.Read())
		{
			if (reader.TokenType == JsonTokenType.EndObject)
				return systemEvent;

			if (reader.TokenType != JsonTokenType.PropertyName)
				throw new JsonException("Expected PropertyName token");

			var propName = reader.GetString();
			reader.Read();

			if (!PropertyMap.ContainsKey(propName))
				throw new JsonException($"JSON contains a property name not found in the type. PropertyName={propName}");

			var property = PropertyMap[propName];

			if (property.PropertyType == typeof(string))
			{
				property.SetValue(systemEvent, reader.GetString());
			}
			else if (property.PropertyType == typeof(DateTimeOffset))
			{
				property.SetValue(systemEvent, reader.GetDateTimeOffset());
			}
		}

		throw new JsonException("Expected EndObject token");
	}
}
Code language: C# (cs)

When writing custom JSON converters, it’s a good idea to make it very specific to the target type you’re converting. In this case, it’s converting the SystemEvent class. This is why this is only dealing with string and DateTimeOffset properties. If you try to make the converter too generic, then it’ll get very complex, very quickly.

It should be noted that this is using a case-insensitive dictionary. This is because JSON properties could have different casing than the properties in the class. For example, the SystemEvent.Name could be “name” in the JSON string.

Using the custom JSON converter

To use the custom JSON converter, add it to JsonSerializerOptions.Converters and pass the options in while serializing and deserializing.

The following example shows it serializing:

var systemEvent = new SystemEvent()
{
	Name = "Meltdown"
};

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

var json = JsonSerializer.Serialize(systemEvent, options);
Code language: C# (cs)

This outputs the following JSON:

{"Name":"Meltdown","HappenedAt":"2021-07-13T10:52:53.9599698-04:00"}Code language: plaintext (plaintext)

Then deserialize this JSON, and passing in the custom JSON converter, with the following:

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

var sysEvent = JsonSerializer.Deserialize<SystemEvent>(json, options);
Code language: C# (cs)

Because the custom JSON converter is being used, it’s able to map the HappenedAt property in the JSON string to the internal property in the SystemEvent object.

Using the JsonInclude attribute

In .NET 5, the JsonInclude attribute was added. This allows you to enable (de)serialization on public properties with non-public accessors. Here’s an example of applying JsonInclude to a property with a private setter:

using System.Text.Json.Serialization;

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

	[JsonInclude]
	public DateTimeOffset HappenedAt { get; private set; }
}
Code language: C# (cs)

To show this working, I’ll show an example of deserializing the following JSON:

{
"Name": "Overload",
"HappenedAt": "2022-02-22T07:42:15.8963892-05:00"
}Code language: JSON / JSON with Comments (json)

Here’s the deserialization:

var sysEvent = JsonSerializer.Deserialize<SystemEvent>(json);

Console.WriteLine(sysEvent.HappenedAt);
Code language: C# (cs)

Here’s what it outputs:

2/22/2022 7:42:15 AM -05:00Code language: plaintext (plaintext)

It successfully populated the HappendAt property, which has a private setter.

Can only apply JsonInclude to public properties

JsonInclude only helps you (de)serialize public properties with non-public accessors. You can’t apply JsonInclude to non-public properties. This constraint is enforced at runtime.

Here’s an example of what happens when you apply JsonInclude to a non-public property:

using System.Text.Json.Serialization;

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

	[JsonInclude]
	internal DateTimeOffset HappenedAt { get; set; }
}
Code language: C# (cs)

When you go to (de)serialize, you’ll get the following runtime exception:

System.InvalidOperationException: The non-public property ‘HappenedAt’ on type ‘SystemEvent’ is annotated with ‘JsonIncludeAttribute’ which is invalid.

This is a very strict constraint. If you want to be able to (de)serialize any property – public or non-public – stick with the custom JSON converter approach. That way you can freely (de)serialize any property you want.