CodeGym /Courses /C# SELF /Serialization of nested and hierarchical objects

Serialization of nested and hierarchical objects

C# SELF
Level 46 , Lesson 2
Available

1. Introduction

Imagine an app for a bookstore: the domain objects aren't just books, but authors with biographies, publishers that release books, employees, sections... If serialization only supported "flat" objects, our app would be stuck at notebook level. In real projects data is almost always multi-level and nested. So knowing how to serialize and deserialize hierarchical structures is a skill that separates an average developer from a true .NET serialization master.

We'll look at how modern C# serializers (using System.Text.Json as an example) let you persist not only object trees but whole "jungles". And how to design classes for serialization so you don't have to manually pluck data apart later.

2. Modeling hierarchical structures

Let's expand our model. In previous examples we had Book, Author and a Library class holding a list of books. Now add another level: let each author have a list of books they wrote (yes, this duplicates info — we'll discuss consequences later!), and make the library hold a list of employees Employee.

Class diagram

Here's roughly how our object structure will look:

classDiagram
    class Library {
        List~Book~ Books
        List~Employee~ Employees
        string Name
    }
    class Book {
        string Title
        Author Author
        int Year
    }
    class Author {
        string Name
        int BirthYear
        List~Book~ Books
    }
    class Employee {
        string Name
        string Position
    }

    Library "1" -- "many" Book
    Library "1" -- "many" Employee
    Book "1" -- "1" Author
    Author "1" -- "many" Book

This approach lets us build a full-featured library system. Yes, there's a risk of "looped" references (for example, an author has books, a book has an author: an infinite serialization spiral!). We'll cover that a bit later.

3. Example class implementations

First let's describe the C# classes for our model. We'll add comments too:

using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;

public class Library
{
    public string Name { get; set; }
    public List<Book> Books { get; set; } = new();
    public List<Employee> Employees { get; set; } = new();
}

public class Book
{
    public string Title { get; set; }
    public int Year { get; set; }

    public Author Author { get; set; }
}

public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
    
    // IMPORTANT! This field is potentially "dangerous": it can create a cyclical reference.
    // But for demonstration we include it.
    public List<Book> Books { get; set; } = new();
}

public class Employee
{
    public string Name { get; set; }
    public string Position { get; set; }
}

4. Building the structure: creating a library

Now let's create a library with books, authors and employees to test serialization.

// Create authors
var author1 = new Author { Name = "William Golding", BirthYear = 1911 };
var author2 = new Author { Name = "John Updike", BirthYear = 1932 };
var author3 = new Author { Name = "J.D. Salinger", BirthYear = 1919 };

// Create books
var book1 = new Book { Title = "Lord of the Flies", Year = 1954, Author = author1 };
var book2 = new Book { Title = "Centaur", Year = 1963, Author = author2 };
var book3 = new Book { Title = "The Catcher in the Rye", Year = 1951, Author = author3 };

// Add books to authors
author1.Books.Add(book1);
author2.Books.Add(book2);
author3.Books.Add(book3);

// Create employees
var emp1 = new Employee { Name = "John Smith", Position = "Librarian" };
var emp2 = new Employee { Name = "Jerry Doe", Position = "Director" };

// Assemble the library
var library = new Library
{
    Name = "City Library",
    Books = new List<Book> { book1, book2, book3 },
    Employees = new List<Employee> { emp1, emp2 }
};

Of course, in real code you'd automate creating objects and linking them so you don't manage each book-author relation manually. But this is fine for our example.

5. Serialization to JSON

We use the classic approach with System.Text.Json:

using System.Text.Json;

// Serialize the library to JSON
var options = new JsonSerializerOptions
{
    WriteIndented = true, // Pretty print with indentation
    ReferenceHandler = ReferenceHandler.IgnoreCycles // Prevent looping!
};

string json = JsonSerializer.Serialize(library, options);

// Print the result
Console.WriteLine(json);

Interesting point: If you don't use the special option ReferenceHandler.IgnoreCycles, serialization will loop — since the author has a list of books and each book has an author, the serializer would keep recursing back and forth until it either crashes or throws an exception. The IgnoreCycles option solves this: when traversing the graph the serializer sees an object already serialized higher in the tree — it writes null instead of serializing it again.

What will the JSON look like?

{
  "Name": "City Library",
  "Books": [
    {
      "Title": "Centaur",
      "Year": 1963,
      "Author": {
        "Name": "John Updike",
        "BirthYear": 1932,
        "Books": [
          {
            "Title": "Centaur",
            "Year": 1963,
            "Author": null
          },
          {
            "Title": "The Witches of Eastwick",
            "Year": 1984,
            "Author": null
          }
        ]
      }
    },
    {
      "Title": "The Witches of Eastwick",
      "Year": 1984,
      "Author": {
        "Name": "John Updike",
        "BirthYear": 1932,
        "Books": [
          {
            "Title": "Centaur",
            "Year": 1963,
            "Author": null
          },
          {
            "Title": "The Witches of Eastwick",
            "Year": 1984,
            "Author": null
          }
        ]
      }
    }
  ],
  "Employees": [
    {
      "Name": "John Smith",
      "Position": "Librarian"
    },
    {
      "Name": "Jerry Doe",
      "Position": "Director"
    }
  ]
}

Note: inside the Books array the author's nested books don't have author info — instead Author is null. That's how the cycle is broken during serialization.

6. Deserialization back to objects

Now let's deserialize the data back:

// Restore object from JSON
var libraryCopy = JsonSerializer.Deserialize<Library>(json, options);

Console.WriteLine(libraryCopy.Name); // "City Library"
Console.WriteLine($"Books: {libraryCopy.Books.Count}");
Console.WriteLine($"Employees: {libraryCopy.Employees.Count}");

But! Restoring cyclic links doesn't work 100% here: nested books in the author's list will have the Author field equal to null, because the serializer cut the chain to prevent infinite nesting.

Important: Serializing complex mutual relationships (for example parent — children — parent) with the standard serializer always requires a compromise: either some relations are lost, or you need to manually rebuild them after deserialization.

7. Cyclic references (nesting vs cycles)

If your structure contains cases where an object references itself through a chain of other objects (a cyclic reference) — standard serializers like System.Text.Json and Newtonsoft.Json, especially in strict typed mode, respond differently. Before ReferenceHandler.IgnoreCycles existed, serialization would stop with an exception like "ReferenceLoopHandling detected". Now it simply writes null instead of the repeating reference.

What's the catch?

Pro: your code won't crash with an error.

Con: after deserialization you often need to manually restore some links. For example, if a serialized graph of users references each other (like employee and their manager) — after deserialization some references can end up empty.

8. How to design complex structures for serialization

If you know in advance that your objects form cycles, or it's important that after restoring the structure all connections remain the same — it's better to store identifiers instead of objects.

Example: store ids instead of references

Change the Book class so the author reference is by id:

public class Book
{
    public string Title { get; set; }
    public int Year { get; set; }

    public int AuthorId { get; set; }
}

Instead of a list of Book objects the author would hold a list of book identifiers. To restore relationships after deserialization you'll need to "match" by id, but then you won't get dangerous cycles.

Why this matters in real projects?

  • Databases almost always use identifiers because they're easier to work with during export/import.
  • REST APIs also exchange ids rather than deeply nested complex structures.

9. Nested collections: serializing trees

A common case is arbitrary-depth hierarchies: folder trees, menu structures, product catalogs with subcategories.

Example class for a "tree":

public class Folder
{
    public string Name { get; set; }
    public List<Folder> Children { get; set; } = new();
}

Build a tree:

var root = new Folder
{
    Name = "Root",
    Children = new List<Folder>
    {
        new Folder { Name = "Sub1", Children = { new Folder { Name = "Sub1-1" } } },
        new Folder { Name = "Sub2" }
    }
};

Serialize and print:

string jsonTree = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonTree);

The JSON for such a tree is a clear hierarchy with nested arrays.

10. Particulars of serializing arrays and lists

If some property is an array (T[]) or a collection (List<T>), serialization will turn it into a normal JSON array.

public class Shop
{
    public string Name { get; set; }
    public string[] Departments { get; set; }
}
var shop = new Shop
{
    Name = "Supermarket",
    Departments = new[] { "Vegetables", "Fruits", "Meat" }
};

string jsonShop = JsonSerializer.Serialize(shop, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonShop);

JSON will look roughly like this:

{
  "Name": "Supermarket",
  "Departments": [
    "Vegetables",
    "Fruits",
    "Meat"
  ]
}

11. Effect of attributes on nested objects

If you use [JsonIgnore] for properties in nested objects, they will also be omitted from the final JSON regardless of nesting level.

public class SecretBook : Book
{
    [JsonIgnore]
    public string SecretCode { get; set; }
}

This approach is often used to protect private info: if you don't need to serialize some internal data, just add the attribute — and it's omitted.

12. Practical tips

  • In interviews you're often asked: "How to serialize a tree (Tree)?" and "What to do with cyclic references?". Prepare examples with ReferenceHandler.IgnoreCycles and storing identifiers.
  • In commercial projects people serialize orders, invoices, users, product catalogs, complex reports. Nesting is everywhere.
  • If you work with graphs or trees try to avoid cycles or use id-based references.
  • If you use an external API, agree on the format of nested structures in advance to avoid surprises when parsing JSON.
2
Task
C# SELF, level 46, lesson 2
Locked
Object Serialization with Collections
Object Serialization with Collections
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION