DEV Community

Theodoros Bebekis
Theodoros Bebekis

Posted on • Originally published at teonotebook.wordpress.com

Serialize and deserialize JSON using JsonSerializer

This text explores the use of the JsonSerializer .Net class in serializing and deserializing .Net classes to JSON.

.Net types related to serialization are found in the following namespaces

There is of course the excellent Newtonsoft.Json library but JsonSerializer is worth using it since it is the native solution provided by .Net and there is no need to install any NuGet package in order to use it.

JsonSerializer can be used with .Net Core 3.0 and later and with .Net Standard 2.0.

The full text and demo project can be found on GitHub.

Basics

The following code entities are used in this text.

public enum Status
{
    None,
    Pending,
    InProgress,
    AllCompleted
}

public class Part
{
    public string Code { get; set; }
    public decimal Amount { get; set; } 
    public bool IsCompleted { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

JsonSerializer is a static class. No need to create an instance.

// serialization
Part P = new(); 
string JsonText =  JsonSerializer.Serialize(P);

// de-serialization
Part P2 = JsonSerializer.Deserialize<Part>(JsonText);

// or
P2 = JsonSerializer.Deserialize(JsonText, typeof(Part)) as Part;
Enter fullscreen mode Exit fullscreen mode

JsonSerializerOptions

The Serialize() and Deserialize() methods accept a JsonSerializerOptions parameter.

JsonSerializerOptions JsonOptions = new();

// serialization
Part P = new(); 
string JsonText = JsonSerializer.Serialize(P, JsonOptions);

// de-serialization
Part P2 = JsonSerializer.Deserialize<Part>(JsonText, JsonOptions);

// or
P2 = JsonSerializer.Deserialize(JsonText, typeof(Part), JsonOptions) as Part;
Enter fullscreen mode Exit fullscreen mode

The JsonSerializerOptions controls the behavior of the JsonSerializer. It provides a great number of properties in order to customize the serialization operation.

Notable properties worth exploring are:

An example of creating a JsonSerializerOptions instance.

JsonSerializerOptions Result = new();

Result.PropertyNamingPolicy = null;
Result.PropertyNameCaseInsensitive = true;
Result.WriteIndented = true;
Result.IgnoreReadOnlyProperties = true;
Result.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;  
Result.ReadCommentHandling = JsonCommentHandling.Skip;
Result.AllowTrailingCommas = true;
Result.NumberHandling = JsonNumberHandling.AllowReadingFromString;
Result.ReferenceHandler = ReferenceHandler.Preserve;  

return Result;
Enter fullscreen mode Exit fullscreen mode

Attributes

The System.Text.Json.Serialization namespace provides a great number of attributes that can be used with properties or classes in order to control the serialization.

Notable attributes worth exploring are:

[JsonConstructor]

Indicates a constructor that should be used by the serializer.

public class Part
{
    public Part() 
    {
    }
    [JsonConstructor]
    public Part(string code) 
    {
       Code = code;
    }
    public string Code { get; set; }  
    public decimal Amount { get; set; }  
    public bool IsCompleted { get; set; } 
}
Enter fullscreen mode Exit fullscreen mode

[JsonConverter]

Specifies what converter type to be used in serialization.

[JsonConverter(typeof(JsonStringEnumConverter))]
public Status Status { get; set; }

[JsonConverter(typeof(DateOnlyConverter))]
public DateOnly BirthDate { get; set; }
Enter fullscreen mode Exit fullscreen mode

The [JsonConverter(typeof(JsonStringEnumConverter))] can be used with enum types also.

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Status
{
    None,
    Pending,
    InProgress,
    AllCompleted
}
Enter fullscreen mode Exit fullscreen mode

[JsonIgnore]

Indicates that the property should be ignored in serialization.

[JsonIgnore]
public string Secret { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Message { get; set; } // ignored when null, the default

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Value { get; set; } // ignored when 0, the default

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public int Value2 { get; set; } =  123; // ignored when 123, the default
Enter fullscreen mode Exit fullscreen mode

[JsonInclude]

Forces serialization of a public field or a public property even when it has just a private setter.

[JsonInclude]
public int Age;

[JsonInclude]
public string ReadOnlyProperty { get; private set; }
Enter fullscreen mode Exit fullscreen mode

[JsonNumberHandling]

Controls how a number is serialized or deserialized by using a JsonNumberHandling setting.

[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public int Amount { get; set; }
Enter fullscreen mode Exit fullscreen mode

[JsonPropertyName]

Controls the name under which the property is serialized to or deserialized from.

[JsonPropertyName("user_name")]
public string UserName { get; set; }
Enter fullscreen mode Exit fullscreen mode

[JsonPropertyOrder]

Controls the serialization order of the property.

[JsonPropertyOrder(5)]
public string Name { get; set; }
Enter fullscreen mode Exit fullscreen mode

[JsonRequired]

Dictates that the property must be present.

[JsonRequired]
public string Name { get; set; }
Enter fullscreen mode Exit fullscreen mode

Attributes from other namespaces

None of the following attributes is used in System.Text.Json serialization.

Only the attributes found in System.Text.Json.Serialization are taken into account. The System.Text.Json namespace has its own attributes, all with Json as prefix.

Except of the System.Text.Json there are some other namespaces providing validation attributes.

  • System.ComponentModel.DataAnnotations namespace. Provides a number of attributes such as [MaxLength], [Required], [Range], etc. Used in Binary Serialization or SOAP Serialization. Also used in Asp.Net Core MVC and WebAPI model validation.
  • System.Runtime.Serialization namespace. Provides attributes such as [DataContract] and [DataMember]. Used by DataContractSerializer.
  • System.Xml.Serialization namespace. Provides attributes such as [XmlRoot], [XmlElement], [XmlAttribute]. Used in XML Serialization.

The static System.ComponentModel.DataAnnotations.Validator class can be used to manually validate classes annotated with ValidationAttribute derived attributes, such as MaxLength and Range.

MyModel Model = JsonSerializer.Deserialize<MyModel>(JsonText);

List<string> ErrorList = Validate(Model);

...

public List<string> Validate(object Instance)
{
    List<string> ErrorList = new();
    var validationContext = new ValidationContext(Instance);
    var validationResults = new List<ValidationResult>();

    // not valid?
    // collect validation errors in a list
    if (!Validator.TryValidateObject(Instance, validationContext, validationResults, true))
    {
        foreach (var validationResult in validationResults)
        {
            ErrorList.Add(validationResult.ErrorMessage);
        }        
    }

    return ErrorList; // empty list means Instance is valid
}
Enter fullscreen mode Exit fullscreen mode

In Asp.Net Core MVC or WebAPI controllers the ModelState.IsValid is used to validate attributes based on System.ComponentModel.DataAnnotations namespace annotations.

Customize serialization with Converters

A converter converts an object or a value to and from JSON text.

A custom converter can also be used.

[JsonConverter(typeof(CustomDateOnlyConverter))]
public DateOnly BirthDate { get; set; }

...

public class CustomDateOnlyConverter : JsonConverter<DateOnly>
{
    public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateOnly.Parse(reader.GetString());
    }

    public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("yyyy-MM-dd"));
    }
}
Enter fullscreen mode Exit fullscreen mode

There are two patterns in creating a custom converter

A custom converter can be registered

  • by adding an instance of the custom converter to JsonSerializerOptions.Converters collection
  • by applying the [JsonConverter] to a class that represents a custom value type
  • by applying the [JsonConverter] to properties that require the custom converter.

When there are multiple converters applied then there are rules that dictate the order by which a converter is chosen in serialization.

Customize serialization with a Resolver and Modifier functions

The JsonSerializerOptions.TypeInfoResolver property provides a way to plug modifier functions of the type Action<JsonTypeInfo> into serialization.

Inline modifiers with lambda functions.

JsonSerializerOptions JsonOptions = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers =
        {
            (TypeInfo) => {
                //...
            },
            (TypeInfo) => {
                //...
            }
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Or usual methods in a class.

static public class Helper
{
    static public void ModifierFunc1(JsonTypeInfo TypeInfo)
    {
        //...
    }
    static public void ModifierFunc2(JsonTypeInfo TypeInfo)
    {
        //...
    }
}

JsonSerializerOptions JsonOptions = new();
Json.Options.Modifiers.Add(Helper.ModifierFunc1);
Json.Options.Modifiers.Add(Helper.ModifierFunc2);
Enter fullscreen mode Exit fullscreen mode

Here is a custom resolver that excludes a list of specified properties from serialization.

public class ExcludePropertiesTypeInfoResolver : DefaultJsonTypeInfoResolver
{
    string[] ExcludeProperties = new string[0];

    static void RemoveAll<T>(IList<T> list, Predicate<T> predicate)
    {
        for (int i = 0; i < list.Count; i++)
        {
            if (predicate(list[i]))
            {
                list.RemoveAt(i--);
            }
        }
    }
    void ModifierFunc(JsonTypeInfo TypeInfo)
    {
        if (TypeInfo.Kind != JsonTypeInfoKind.Object)
            return;

        RemoveAll(TypeInfo.Properties, prop => ExcludeProperties.Contains(prop.Name)); 
    }

    public ExcludePropertiesTypeInfoResolver(string[] ExcludeProperties)
    {
        this.ExcludeProperties = ExcludeProperties;
        this.Modifiers.Insert(0, ModifierFunc);
    }
}
Enter fullscreen mode Exit fullscreen mode

And here is how to use it.

var ExcludeProperties = new[] { "Prop1", "Prop2"};

JsonSerializerOptions JsonOptions = new()
{
    TypeInfoResolver = new ExcludePropertiesTypeInfoResolver(ExcludeProperties)
}; 
Enter fullscreen mode Exit fullscreen mode

The JsonTypeInfo provides metadata about the type being serialized.

Regarding modifier functions .Net Core docs provide a number of examples.

Property Name Casing

The casing of a property name, such as camel-casing, is controlled by the JsonSerializerOptions.PropertyNamingPolicy.

PropertyNamingPolicy property is a JsonNamingPolicy derived class. When it is null, the default, property names remain unchanged.

That JsonNamingPolicy class provides a number of static properties that return a JsonNamingPolicy derived class instance, such as CamelCase or SnakeCaseLower. Each one for a specific casing.

JsonSerializerOptions JsonOptions = new();

JsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CameCase; 
Enter fullscreen mode Exit fullscreen mode

The PopulateObject() problem.

System.Text.Json provides a solution under the title Populate initialized properties which is far from what is actully needed.

Frequently there are cases where there is an already constructed instance that needs to be populated using data coming as json text. This is a problem that the System.Text.Json has no solution to offer yet.

The JsonSerializer.Deserialize() methods always create and return a new instance. This is not always what an application needs.

But deep in the .Net source code there is a class containing a method that do just that. The Microsoft.Graph.DerivedTypeConverter contains a private method, named PopulateObject() which does exactly what it says.

The project that accompanies this text contains a static class under the name NetJson which provides, among other useful utilities, a PopulateObject() method. This PopulateObject() method is just the code from the Microsoft.Graph.DerivedTypeConverter.PopulateObject() private method.

static public void PopulateObject(object Instance, string JsonText, JsonSerializerOptions Options)

The Document Object Model (DOM) of System.Text.Json

Except of the JsonSerializer the .Net Core serialization sub-system provides a DOM model too.

The system provides two ways in building a DOM model.

JsonObject, JsonNode, JsonArray and JsonValue

These are mutable classes, meaning the application may add, modify or remove elements in the DOM tree.

  • JsonNode is an abstract class. Besides that it provides a great number of static helper methods for adding elements to the DOM tree. It also serves as the base class for the others in this group.
  • JsonObject represents a mutable DOM object.
  • JsonArray represents a mutable DOM array object.
  • JsonValue represents a mutable DOM value object.

All the above provide the following properties

  • an integer indexer property Item[Int32]
  • a string indexer property Item[String]
  • a Count property

All the above provide the following methods

  • AsObject()
  • AsArray()
  • AsValue()
  • GetPath()
  • GetPropertyName()
  • GetType()
  • GetValue<T>()
  • GetValueKind() which returns a value of the JsonValueKind enum
  • ReplaceWith<T>(T)
  • ToJsonString()

The JsonObject and JsonArray provide the following methods too

  • Add()
  • Clear()
  • Remove()
  • RemoveAt()

Primitive Values using JsonValue.Create(AnyPrimitiveValue)

JsonObject properties are Key-Value pairs, i.e. JORoot.Add(PropertyName, PropertyValue).

The JsonValue.Create(AnyPrimitiveValue) is used with primitive Types such as string and integer, in creating new JsonValue objects.

JsonObject JORoot = new();
JORoot.Add("String", JsonValue.Create("This is a JsonValue"));
JORoot.Add("DateTime", JsonValue.Create(DateTime.Now));
JORoot.Add("Integer", JsonValue.Create(123));
Enter fullscreen mode Exit fullscreen mode

Removing a Property

Using the Remove(PropertyName) removes the property.

JORoot.Remove("DateTime");
Enter fullscreen mode Exit fullscreen mode

JsonObject is a Dictionary-like object

JsonObject instances are Dictionary-like objects and a new JsonObject can be initialized as following.

JsonObject JORoot = new JsonObject
{
    ["Key1"] = "This is a string",
    ["Key2"] = DateTime.Now,
    ["Key3"] = false,
};
Enter fullscreen mode Exit fullscreen mode

The JsonNode.Parse() method

The JsonNode.Parse(JsonText) static method parses text and returns a JsonNode object.

JsonObject JORoot = new();
JORoot.Add("Person", JsonNode.Parse("""{ "Name": "John Doe", "Age": 30 }"""));
Enter fullscreen mode Exit fullscreen mode

Using the JsonValue.Create<T>() with non-primitive Types

The JsonValue.Create<T>() is used with non-primitive Types such as user defined classes, in creating new JsonValue objects.

JsonObject JORoot = new();
JORoot.Add("Part", JsonValue.Create<Part>(new Part()));
Enter fullscreen mode Exit fullscreen mode

Initializing a JsonArray

JsonObject JORoot = new();
JsonArray JOArray = new JsonArray() { 123, true, DateTime.Now, "string value" };
JORoot.Add("Array", JOArray);
Enter fullscreen mode Exit fullscreen mode

Using a Dictionary

A Dictionary<string, JsonNode> can be used in adding a property to a JsonObject object.

The Dictionary<string, JsonNode> dictionary has to be converted to a JsonObject first.

This can be done because JsonObject provides a suitable constructor.

public JsonObject(IEnumerable<KeyValuePair<string, JsonNode?>> properties, JsonNodeOptions? options = null)

JsonObject JORoot = new();

var Dictionary = new Dictionary<string, JsonNode>
{
    ["Key1"] = "This is a string",
    ["Key2"] = DateTime.Now,
    ["Key3"] = false,
    ["Key4"] = JsonValue.Create<Status>(Status.InProgress),
};

JsonObject DicNode = new JsonObject(Dictionary);
JORoot.Add("Dictionary", DicNode);
Enter fullscreen mode Exit fullscreen mode

Accessing Properties and Values

string DemoJsonText = """
        {
            "Id": 1,
            "Name": "Model 1",
            "Status": "InProgress",
            "Active": true,
            "Parts": [
                {
                "Code": "001",
                "Amount": 1.2,
                "IsCompleted": true
                },
                {
                "Code": "002",
                "Amount": 3.4,
                "IsCompleted": false
                }
            ],
            "Properties": {
            "John": "Doe",
            "NiceCar": "Volvo"
            },
            "DT": "2025-06-04T00:59:25.6948527+03:00"
        }
        """;

JsonNode RootNode = JsonNode.Parse(DemoJsonText);

// accessing array
JsonNode PartsNode = RootNode["Parts"];
JsonNode FirstPartNode = PartsNode[0]; 

// adding new property
JsonObject ThirdPartNode = new();
ThirdPartNode["Code"] = "003";
ThirdPartNode["Amount"] = 12.3;
ThirdPartNode["IsCompleted"] = true;

// get node as array
JsonArray ArrayNode = PartsNode.AsArray();
ArrayNode.Add(ThirdPartNode);

// typecasting nodes
JsonNode PropNode = ThirdPartNode["Amount"];
double V = (double)PropNode;
// 12.3

string S = (string)ThirdPartNode["Code"];
// 003

// using the GetValue<T>()
DateTime DateTimeNode = RootNode["DT"].GetValue<DateTime>();

// get the path
string S2 = RootNode["Parts"][0].GetPath();
// $.Parts[0]   where $ denotes the root node
Enter fullscreen mode Exit fullscreen mode

JsonDocument

JsonDocument is used in building a read-only DOM. It provides the RootElement of type JsonElement.

JsonDocument elements are accessed using the JsonElement class.

The JsonElement class provides enumerators in order to iterate over its elements.

The JsonElement class provides methods such as GetInt32() and TryGetInt32() which convert JSON text to .Net primitive types.

NOTE: JsonElement is an IDisposable type.

double Total = 0;

using (JsonDocument Doc = JsonDocument.Parse(DemoJsonText))
{
    JsonElement Root = Doc.RootElement;
    JsonElement PartsProperty = Root.GetProperty("Parts");
    foreach (JsonElement PartProperty in PartsProperty.EnumerateArray())
    {
        if (PartProperty.TryGetProperty("Amount", out JsonElement AmountElement))
        {
            Total += AmountElement.GetDouble();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)