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

By default, the System.Text.Json.JsonSerializer only serializes public properties. If you want to serialize non-public properties, you can create a custom converter and use reflection to get the non-public properties too, like this:

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 converter by adding it to JsonSerializerOptions.Converters and passing the options in 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 converter that serializes and deserializes non-public properties

Let’s say we want to serialize and deserialize SystemEvent objects, and we want to include all non-public properties. SystemEvent has the following definition:

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 converter

The following custom converter uses reflection to get all of SystemEvent’s properties, public and non-public.

In the constructor, it’s using reflection to lookup all the properties. It caches these in a dictionary to save on lookup costs during deserialization.

Write() serializes by looping over the reflected properties and writing their values with the Utf8JsonWriter object.

Read() deserializes by looping through the JSON properties and matching them to the reflected properties. It uses the reflected properties to assign the value from the Utf8JsonReader object to the SystemEvent 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 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 be cased differently than the properties in the class. For example, the SystemEvent.Name could be “name” in the JSON string.

Using the custom converter

To use the custom 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 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 converter is being used, it’s able to map the HappenedAt property in the JSON string to the internal property in the SystemEvent object.

Leave a Comment