How to serialize non-public properties using System.Text.Json
By FoxLearn 3/14/2025 3:30:37 AM 120
- Create a custom JSON converter and use reflection to retrieve the non-public properties.
- Use the
JsonInclude
attribute to enable (de)serialization of public properties with non-public accessors (this does not apply to non-public properties).
In this article, I’ll demonstrate both approaches for handling non-public properties.
Write a custom JSON converter to serialize non-public properties
When the built-in System.Text.Json
functionality doesn’t quite meet your needs, you can create a custom JSON converter. In this case, to serialize non-public properties, you can write a custom JSON converter to (de)serialize all the properties you want including non-public ones.
Here’s an example of a custom JSON converter that handles both 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(); } }
To use this custom JSON converter, add it to JsonSerializerOptions.Converters
and pass the JsonSerializerOptions
during serialization:
var options = new JsonSerializerOptions(); options.Converters.Add(new CustomPersonConverter()); var json = JsonSerializer.Serialize(person, options);
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 ones. We’ll use the following type, SystemEvent
:
public class SystemEvent { public string Name { get; set; } internal DateTimeOffset HappenedAt { get; set; } public SystemEvent() { HappenedAt = DateTimeOffset.Now; } }
This example assumes the internal property cannot be made public.
Custom JSON converter
The custom JSON converter uses reflection to retrieve all properties both public and non-public:
- The constructor uses reflection to gather properties into a dictionary to avoid repeated lookups during deserialization.
Write()
loops through the dictionary and serializes each property usingUtf8JsonWriter
.Read()
reads through the JSON properties withUtf8JsonReader
and updates the object properties accordingly.
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() { 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"); } }
To use the custom JSON converter for serialization, you need to add it to JsonSerializerOptions.Converters
:
var systemEvent = new SystemEvent() { Name = "Meltdown" }; var options = new JsonSerializerOptions(); options.Converters.Add(new CustomSystemEventConverter()); var json = JsonSerializer.Serialize(systemEvent, options);
This will serialize to:
{"Name":"Meltdown","HappenedAt":"2025-03-13T10:52:53.9599698-04:00"}
Then, to deserialize the JSON, use the same custom converter:
var sysEvent = JsonSerializer.Deserialize<SystemEvent>(json, options);
Using the JsonInclude
attribute
Starting in .NET 5, the JsonInclude
attribute was introduced. This enables (de)serialization for public properties with non-public accessors.
Here’s an example of using JsonInclude
with a property that has a private setter:
using System.Text.Json.Serialization; public class SystemEvent { public string Name { get; set; } [JsonInclude] public DateTimeOffset HappenedAt { get; private set; } }
For deserialization, this JSON will work:
{ "Name": "Overload", "HappenedAt": "2025-02-22T07:42:15.8963892-05:00" }
Deserialization example:
var sysEvent = JsonSerializer.Deserialize<SystemEvent>(json); Console.WriteLine(sysEvent.HappenedAt);
Output:
2/22/2025 7:42:15 AM -05:00
This shows the HappenedAt
property, which has a private setter, being populated correctly.
Can only apply JsonInclude
to public properties
It’s important to note that JsonInclude
only works for public properties with non-public accessors. You cannot apply JsonInclude
to non-public properties.
using System.Text.Json.Serialization; public class SystemEvent { public string Name { get; set; } [JsonInclude] internal DateTimeOffset HappenedAt { get; set; } }
Attempting to (de)serialize will throw the following exception:
System.InvalidOperationException: The non-public property ‘HappenedAt’ on type ‘SystemEvent’ is annotated with ‘JsonIncludeAttribute’ which is invalid.
- Primitive types in C#
- How to set permissions for a directory in C#
- How to Convert Int to Byte Array in C#
- How to Convert string list to int list in C#
- How to convert timestamp to date in C#
- How to Get all files in a folder in C#
- How to use Channel as an async queue in C#
- Case sensitivity in JSON deserialization