Understanding Object Serialization

The .NET Runtime will account for all related objects to ensure that public data is persisted correctly when an object is serialized. This set of related objects is referred to as an object graph.

You can read the arrows in an object diagram as "required" or "depends on".

Each object in an object graph is assigned a unique numerical value.

You have a base class named Car, which has-a Radio. Another class named JamesBondCar extends the Car base type.

Of course, the CLR does not point pictures in memory to represent a graph of related objects. Rather, the relationship documented in the Figure is represented by a mathematical formula that looks something like this:

[Car 3, ref 2], [Radio 2], [JamesBondCar 1, ref 3, ref 2]

Creating the Sample Types and Top-Level Statements

public class Radio
 {
     public bool HasTweeters;
     public bool HasSubWoofers;
     public List<double> StationPresets;
     public string RadioId = "XF-552RR6";
     public override string ToString()
     {
         var presets = string.Join(",", StationPresets.Select(i => i.ToString()).ToList());
         return $"HasTweeters:{HasTweeters} HasSubWoofers:{HasSubWoofers} Station Presets:{presets}";
     }
 }
 
public class Car
 {
     public Radio TheRadio = new Radio();
     public bool IsHatchBack;
     public override string ToString()
     => $"IsHatchback:{IsHatchBack} Radio:{TheRadio.ToString()}";
 }
 
public class JamesBondCar : Car
 {
     public bool CanFly;
     public bool CanSubmerge;
     public override string ToString() => $"CanFly:{CanFly}, CanSubmerge:{CanSubmerge} {base.ToString()}";
 }
 
public class Person
 {
     // A public field.
     public bool IsAlive = true;
     // A private field.
     private int PersonAge = 21;
     // Public property/private data.
     private string _fName = string.Empty;
      
     public string FirstName
     {
     get { return _fName; }
     set { _fName = value; }
     }
     public override string ToString() =>$"IsAlive:{IsAlive} FirstName:{FirstName} Age:{PersonAge} ";
 }

In Program.cs

var theRadio = new Radio
 {
     StationPresets = new() { 89.3, 105.1, 97.1 },
     HasTweeters = true
 };
     // Make a JamesBondCar and set state.
 JamesBondCar jbc = new()
 {
        CanFly = true,
        CanSubmerge = false,
        TheRadio = new()
        {
            StationPresets = new() { 89.3, 105.1, 97.1 },
            HasTweeters = true
        }
 };
List<JamesBondCar> myCars = new()
 {
      new JamesBondCar { CanFly = true, CanSubmerge = true, TheRadio = theRadio },
      new JamesBondCar { CanFly = true, CanSubmerge = false, TheRadio = theRadio },
      new JamesBondCar { CanFly = false, CanSubmerge = true, TheRadio = theRadio },
      new JamesBondCar { CanFly = false, CanSubmerge = false, TheRadio = theRadio },
 };
 Person p = new Person
 {
      FirstName = "James",
      IsAlive = true
 };

Extensible Markup Language (XML)

One of the original goals of XML was to represent an object (or set of objects) in human- and machine-

readable format. An XML document is a single file that contains the item(s) being serialized.

Serializing and Deserializing with the XMLSerializer


The System.Xml namespace provides the System.Xml.Serialization.XmlSerializer. You can use this

formatter to persist the public state of a given object as pure XML. Note that the XmlSerializer requires you

to declare the type that will be serialized (or deserialized).

Controlling the Generated XML Data

XmlSerializer serializes all public fields/properties as XML elements, rather than as XML attributes.

Select Attributes of the System.Xml.Serialization Namespace

  1. [XmlAttribute] : You can use this .NET attribute on a public field or property in a class to tell

XmlSerializer to serialize the data as an XML attribute (rather than as a subelement).

  1. [XmlElement] : The field or property will be serialized as an XML element named as you so choose.
  2. [XmlEnum] : This attribute provides the element name of an enumeration member.
  3. [XmlRoot] : This attribute controls how the root element will be constructed (namespace and

element name).

  1. [XmlText] : The property or field will be serialized as XML text (i.e., the content between the start

tag and the end tag of the root element).

  1. [XmlType] : This attribute provides the name and namespace of the XML type.

Serializing Objects Using the XmlSerializer

static void SaveAsXmlFormat<T>(T objGraph, string fileName)
 {
     //Must declare type in the constructor of the XmlSerializer
     XmlSerializer xmlFormat = new XmlSerializer(typeof(T));
     using (Stream fStream = new FileStream(fileName,
     FileMode.Create, FileAccess.Write, FileShare.None))
     {
         xmlFormat.Serialize(fStream, objGraph);
     }
 }
 
SaveAsXmlFormat(jbc, "CarData.xml");
SaveAsXmlFormat(p, "PersonData.xml");

If you want to specify a custom XML namespace that qualifies the JamesBondCar and encodes the

canFly and canSubmerge values as XML attributes instead of elements, you can do so by modifying the C#

definition of JamesBondCar like this:

[Serializable, XmlRoot(Namespace = "http://www.MyCompany.com")]
public class JamesBondCar : Car
 {
     [XmlAttribute]
     public bool CanFly;
     [XmlAttribute]
     public bool CanSubmerge;
     ...
 }

XML Serialization serializes only public properties and fields.

Serializing Collections of Objects

SaveAsXmlFormat(myCars,"CarCollection.xml");

Deserializing Objects and Collections of Objects

XML deserialization is literally the opposite of serializing objects (and collections of objects).

static T ReadAsXmlFormat<T>(string fileName)
 {
     // Create a typed instance of the XmlSerializer
     XmlSerializer xmlFormat = new XmlSerializer(typeof(T));
     using (Stream fStream = new FileStream(fileName, FileMode.Open))
     {
         T obj = default;
         obj = (T)xmlFormat.Deserialize(fStream);
         return obj;
     }
 }

JamesBondCar savedCar = ReadAsXmlFormat<JamesBondCar>("CarData.xml");

JavaScript Object Notation (JSON) Serialization

Objects in JSON documents are designated using name-value pairs for the properties enclosed in curly

braces ({}).

{
"firstName": "James",
"isAlive": true
}

List of objects are stored as JS arrays using square brackets ([]).

Serializing and Deserializing with System.Text.Json

The System.Text.Json namespace provides the System.Text.Json.JsonSerializer. You can use this formatter to persist the public state of a given object as JSON.

Controlling the Generated JSON Data

By default, JsonSerializer serializes all public properties as JSON name-value pairs using the same name

(and casing) of the object’s property names. You can control many aspects of the serialization process with

attributes.

Select Attributes of the System.Text.Json.Serialization Namespace

  1. [JsonIgnore] : The property will be ignored.
  2. [JsonInclude] : The member will be included.
  3. [JsonPropertyName] : This specifies the property name to be used when serializing/deserializing a

member. This is commonly used to resolve character casing issues.

  1. [JsonConstructor] : This indicates the constructor that should be used when deserializing JSON back

into an object graph.

Serializing Objects Using the JsonSerializer

static void SaveAsJsonFormat<T>(T objGraph, string fileName)
 {
     File.WriteAllText(fileName, System.Text.Json.JsonSerializer.Serialize(objGraph));
 }
SaveAsJsonFormat(jbc, "CarData.json");
SaveAsJsonFormat(p, "PersonData.json");

The JsonSerializer only writes the public props by default, and not public fields.

Including Fields

static void SaveAsJsonFormat<T>(T objGraph, string fileName)
 {
     var options = new JsonSerializerOptions
     {
         IncludeFields = true,
     };
     File.WriteAllText(fileName, System.Text.Json.JsonSerializer.Serialize(objGraph, options));
 }

Instead of using the JsonSerializerOptions, you can achieve the same result by updating all public

fields in the sample classes

public class Radio
 {
     [JsonInclude]
     public bool HasTweeters;
     [JsonInclude]
     public bool HasSubWoofers;
     [JsonInclude]
     ...
 }

Pretty-Print the JSON

The JsonSerializer can be instructed to write the JSON indented (and human readable).

static void SaveAsJsonFormat<T>(T objGraph, string fileName)
 {
     var options = new JsonSerializerOptions
     {
         IncludeFields = true,
         WriteIndented = true
     };
     File.WriteAllText(fileName, System.Text.Json.JsonSerializer.Serialize(objGraph, options));
 }

PascalCase or camelCase JSON

If no naming policy is specified, the JsonSerializer will use camel casing when serializing

and deserializing JSON. To change the serialization process to use Pascal casing, you need to set

PropertyNamingPolicy to null;

static void SaveAsJsonFormat<T>(T objGraph, string fileName)
 {
     JsonSerializerOptions options = new()
     {
         PropertyNamingPolicy = null,
         IncludeFields = true,
         WriteIndented = true,
     };
 }

There is another option when deserializing JSON, and that is casing indifference. By setting the PropertyNameCaseInsensitive option to true, then C# will deserialize canSubmerge as well as CanSubmerge.

JsonSerializerOptions options = new()
 {
     PropertyNameCaseInsensitive = true,
     IncludeFields = true
 };

Ignoring Circular References with JsonSerializer

JsonSerializer supports ignoring circular references when serializing an object graph. This is done by setting the ReferenceHandler to ReferenceHandler.

JsonSerializerOptions options = new()
 {
     ReferenceHandler = ReferenceHandler.IgnoreCycles
 };

  1. IgnoreCycles : Circular references are not serialized, and the reference loop is replaced with a null.
  2. Preserve : Metadata properties will be honored when deserializing JSON objects and arrays into

reference types and written when serializing reference types. This is necessary to create

round-trippable JSON from objects that contain cycles or duplicate references.

Number Handling with JsonSerializer

The default handling of numbers is Strict, meaning numbers will be serialized as numbers (without

quotes) and deserialized as numbers (without quotes). Use the NumberHandling property that controls reading and writing numbers.

  1. Strict (0) : Numbers are read from numbers and written as numbers. Quotes are not allowed nor are they generated.
  2. AllowReadingFromString (1) : Numbers can be read from number or string tokens.
  3. WriteAsString (2) : Numbers are written as JSON strings (with quotes).
  4. AllowNamedFloatingPointLiterals (4) : The Nan, Infinity, and -Infinity string tokens can be read, and Single and Double values will be written as their corresponding JSON string representations.

JSON Property Ordering

the JsonPropertyOrder attribute controls property ordering during serialization. The smaller the number (including negative values), the earlier the property is in the resulting JSON. Properties without an order are assigned a default order of zero.

public class Person
 {
     [JsonPropertyOrder(1)]
     public bool IsAlive = true;
     private int PersonAge = 21;
     private string _fName = string.Empty;
     [JsonPropertyOrder(-1)]
     public string FirstName
     {
         get { return _fName; }
         set { _fName = value; }
     }
     public override string ToString() => $"IsAlive:{IsAlive} FirstName:{FirstName} Age:{PersonAge} ";
 }

Support for IAsyncEnumerable

JsonSerializer now has support for serializing and deserializing async streams.

Streaming Serialization

static async IAsyncEnumerable<int> PrintNumbers(int n)
 {
     for (int i = 0; i < n; i++)
     {
         yield return i;
     }
 }

async static void SerializeAsync()
 {
     Console.WriteLine("Async Serialization");
     using Stream stream = Console.OpenStandardOutput();
     var data = new { Data = PrintNumbers(3) };
     await JsonSerializer.SerializeAsync(stream, data);
     Console.WriteLine();
 }

Streaming Deserialization

async static void DeserializeAsync()
 {
     Console.WriteLine("Async Deserialization");
     var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("[0,1,2,3,4]"));
     await foreach (int item in JsonSerializer.DeserializeAsyncEnumerable<int>(stream))
     {
         Console.Write(item);
     }
     Console.WriteLine();
 }