1. Introduction
Serializing simple objects is like mailing a postcard: easy, nothing gets lost. But often our objects turn into "family albums": they contain nested objects, collections, and even collections of collections.
Imagine our user is not just a Person with a name and age, but for example has a set of contacts (Contact), several home/work addresses, and maybe a field like List<Pet> if the person loves animals. All of that is nested objects and collections.
How do .NET serializers handle this? What nuances should you consider when serializing complex object trees? Can everything break if there are other collections inside collections? Today we'll figure out what to expect — and what to do in tricky cases.
What "nested" means in .NET terms
A nested object is simply another object as a property or field inside your main object. For example, here's our extended user model:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public List<Contact> Contacts { get; set; } // A collection of nested objects
public Address? HomeAddress { get; set; } // One nested object (nullable)
}
public class Contact
{
public string Type { get; set; } // For example, Email or Phone
public string Value { get; set; }
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
Note — a person can have multiple contacts, and the address is only one (and it can be null). This is classic for many models.
2. Serializing objects and collections with System.Text.Json
Simple example: serialize and deserialize a collection
Let's see how this works in practice:
using System.Text.Json;
var person = new Person
{
Name = "Anna",
Age = 28,
Contacts = new List<Contact>
{
new Contact { Type = "Email", Value = "anna@example.com" },
new Contact { Type = "Phone", Value = "+1234567890" }
},
HomeAddress = new Address { City = "Berlin", Street = "Alexanderplatz, 1" }
};
string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
The result will look roughly like this:
{
"Name": "Anna",
"Age": 28,
"Contacts": [
{
"Type": "Email",
"Value": "anna@example.com"
},
{
"Type": "Phone",
"Value": "+1234567890"
}
],
"HomeAddress": {
"City": "Berlin",
"Street": "Alexanderplatz, 1"
}
}
The serializer understands nesting well. If a property is another object, it serializes it "nested". If it's a list — it serializes it as an array.
Fact: This kind of serialization works automatically for any collections and nested objects if they are public and have public getters and setters (get/set).
Deserialization: works "out of the box"
string jsonInput = /* JSON we just got */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Anna"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"
Everything works — contacts are converted from a JSON array back to List<Contact>, HomeAddress — back into an Address object.
3. How collections are serialized: List, arrays, Dictionary
Often your model contains not only lists (List<T>) but also dictionaries (Dictionary<TKey, TValue>) or multidimensional arrays.
Example with an array
public class Team
{
public string Name { get; set; }
public Person[] Members { get; set; }
}
var team = new Team
{
Name = "Developers",
Members = new[]
{
new Person { Name = "Aleksey", Age = 31 },
new Person { Name = "Ekaterina", Age = 27 }
}
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Name": "Developers",
"Members": [
{
"Name": "Aleksey",
"Age": 31,
"Contacts": null,
"HomeAddress": null
},
{
"Name": "Ekaterina",
"Age": 27,
"Contacts": null,
"HomeAddress": null
}
]
}
Collection serialization is similar to packing a bunch of identical mailings: each element is a separate "postcard".
Example with Dictionary
public class Phonebook
{
public Dictionary<string, string> Phones { get; set; }
}
var phonebook = new Phonebook
{
Phones = new Dictionary<string, string>
{
{ "Andrey", "+12998887766" },
{ "Maria", "+12882223344" }
}
};
string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Phones": {
"Andrey": "+12998887766",
"Maria": "+12882223344"
}
}
In JSON a dictionary becomes an object with dynamic keys.
4. Nested collections (List inside List, arrays of arrays)
.NET can serialize these "matryoshkas" too:
public class Zoo
{
public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }
var zoo = new Zoo
{
AnimalGroups = new List<List<Animal>>
{
new List<Animal> { new Animal { Name = "Lion" }, new Animal { Name = "Tiger" } },
new List<Animal> { new Animal { Name = "Bear" } }
}
};
string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"AnimalGroups": [
[ { "Name": "Lion" }, { "Name": "Tiger" } ],
[ { "Name": "Bear" } ]
]
}
And the same goes for deserialization.
5. Features of serializing nested objects with XmlSerializer
Example of serializing a nested object
using System.Xml.Serialization;
using System.IO;
var person = new Person
{
Name = "Ivan",
Age = 35,
HomeAddress = new Address { City = "Bonn", Street = "Beethoven str., 100" },
Contacts = new List<Contact>
{
new Contact { Type = "Email", Value = "ivan@domain.de" }
}
};
var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());
Result:
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Ivan</Name>
<Age>35</Age>
<Contacts>
<Contact>
<Type>Email</Type>
<Value>ivan@domain.de</Value>
</Contact>
</Contacts>
<HomeAddress>
<City>Bonn</City>
<Street>Beethoven str., 100</Street>
</HomeAddress>
</Person>
Collection specifics in XmlSerializer
By default XmlSerializer serializes a collection as a "Contacts" tag, inside which each element gets its own "Contact" tag. For that the collection type must be public and its elements must be serializable types.
Important point. If the collection is empty, XML still includes an empty tag:
<Contacts />
Supported collections. XmlSerializer supports List<T>, arrays T[], collections implementing ICollection<T>. It does not support, for example, Dictionary<TKey, TValue>! To serialize dictionaries you'll need to wrap the dictionary into a list of pairs or use other serializers or helper libraries.
6. Support for objects and collections in Newtonsoft.Json
Newtonsoft.Json supports nesting a bit more flexibly. In 99% of cases everything works the same as in System.Text.Json, but there are advantages: you can serialize private fields (if explicitly configured), dictionaries with more complex keys, dynamic types, and even circular references (with special settings).
Example
using Newtonsoft.Json;
var person = new Person
{
Name = "Pavel",
Age = 40,
Contacts = new List<Contact>
{
new Contact { Type = "SMS", Value = "+10000000013" }
}
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);
Difference. If an object has, say, private fields, they can be serialized via a ContractResolver setting. But for basic nested collections and objects everything works automatically "out of the box".
7. Common mistakes, pitfalls and nuances
Error 1: Nested objects are null.
If some properties of the object you're serializing are not set (null), they may be missing from JSON or XML, or they may be recorded as null. For example, if HomeAddress on Person is null, then in JSON:
"HomeAddress": null
In XML by default that tag won't exist — you can add it with serializer settings ([XmlElement(IsNullable=true)]).
Error 2: Serializing private properties/fields.
By default most serializers (both JSON and XML) work only with public properties. If you want to serialize private or protected fields, you'll need to make them public or configure the serializer (for example, via ContractResolver in Newtonsoft.Json).
Error 3: Classes without a parameterless constructor.
In XML serialization (and often in JSON) a class should have a public parameterless constructor. If it doesn't, you'll get an exception during deserialization.
Error 4: Serializing dictionaries to XML.
XmlSerializer doesn't support serializing Dictionary<TKey, TValue> out of the box. This often comes as a surprise. The solution can be wrapping the dictionary into a list of pairs or using other serializers.
Error 5: Circular references.
If one object references another object which in turn references the first (for example, parent and child), serializing "in a circle" can lead to a stack overflow error (StackOverflowException) or an exception about a circular reference. In JSON serializers this can be worked around with settings, but often it's better to revisit the object architecture.
GO TO FULL VERSION