The previous lesson saved BiblioTech's catalog to a plain text file with manual, field-by-field parsing, separated by ;. It works, but it's fragile: a title with a ; would break the format, and every new data structure (loans, members with more properties...) would demand inventing and maintaining a different text format by hand. Serialization solves this in a general way: it converts any object into a reusable representation (nowadays, almost always JSON) without the programmer having to design or parse a custom format. This lesson introduces System.Text.Json, .NET's standard library for working with JSON, and replaces the previous lesson's manual parsing with real serialization of Library.Catalog and Library.Members.

Contents

  1. What serialization is and what problem it solves
  2. A look at the past: why JSON won out over binary and XML
  3. System.Text.Json: serializing with JsonSerializer.Serialize
  4. Deserializing with JsonSerializer.Deserialize
  5. Customizing the JSON: JsonSerializerOptions and [JsonPropertyName]
  6. The inheritance challenge: serializing a catalog with Book and Magazine
  7. Persisting all of Library in JSON

  1. What serialization is and what problem it solves

Serializing an object means converting its state (the value of its properties) into a sequence of data that can be saved to a file, sent over a network, or stored in a database. Deserializing is the reverse operation: rebuilding an object from that representation. It's exactly what the previous lesson did by hand with ; as a separator, only now it's delegated to a library that already solves, in a generic way, cases a manual parser handles poorly: text with special characters, nested lists, missing values...

flowchart LR
    A[Object in memory] -->|Serialize| B[Text / JSON]
    B -->|Deserialize| C[Rebuilt object]

  1. A look at the past: why JSON won out over binary and XML

.NET has historically had several serialization mechanisms:

Format Human-readable Current status
Binary (BinaryFormatter) No Obsolete and retired for serious security reasons (it allowed running arbitrary code when deserializing untrusted data); must not be used in new code
XML (XmlSerializer, DataContractSerializer) Yes, but verbose Still around, used mostly in legacy systems or formats that already require it (like some SOAP services)
JSON (System.Text.Json, previously Newtonsoft.Json) Yes, and compact Today's de facto standard

JSON has become the default format for several practical reasons: it's more compact than XML, it's directly readable by a person (unlike binary), it's JavaScript's native format and therefore that of practically every modern web API, and System.Text.Json (included in .NET since version 3.0, with no need to install any extra package) offers far better performance than the historical alternatives. That's why this lesson — and the module's last one, on REST APIs — focus exclusively on JSON.

  1. System.Text.Json: serializing with JsonSerializer.Serialize

The entry point for serializing any object is the static method JsonSerializer.Serialize, from the System.Text.Json namespace:

using System.Text.Json;

Member member = new Member(1, "Ana Martinez");
string json = JsonSerializer.Serialize(member);

Console.WriteLine(json);
// {"Id":1,"Name":"Ana Martinez"}

JsonSerializer.Serialize walks, through reflection (a technique you'll see in detail in Module 6), every public property of the object and produces equivalent JSON text. There's no need to write any manual conversion code: it's enough for the properties to be public, as they already are in Member, Book, and the rest of the BiblioTech model.

It also works directly on entire collections:

List<Member> members = new List<Member>
{
    new Member(1, "Ana Martinez"),
    new Member(2, "Luis Gomez")
};

string membersJson = JsonSerializer.Serialize(members);
Console.WriteLine(membersJson);
// [{"Id":1,"Name":"Ana Martinez"},{"Id":2,"Name":"Luis Gomez"}]

  1. Deserializing with JsonSerializer.Deserialize

The reverse operation, JsonSerializer.Deserialize<T>, rebuilds an object (or a collection) from a JSON text, specifying the target type as a generic type argument (recall generics from Module 4):

string json = "{\"Id\":1,\"Name\":\"Ana Martinez\"}";

Member? member = JsonSerializer.Deserialize<Member>(json);
Console.WriteLine(member?.Name); // "Ana Martinez"

string membersJson = "[{\"Id\":1,\"Name\":\"Ana Martinez\"},{\"Id\":2,\"Name\":\"Luis Gomez\"}]";
List<Member>? deserializedMembers = JsonSerializer.Deserialize<List<Member>>(membersJson);
Console.WriteLine(deserializedMembers?.Count); // 2

Deserialize<T> returns T? (it can return null if the input JSON is literally the string "null"); in practice, if the JSON comes from a file your own program generated, the result will never be null, but the compiler forces you to account for it, just like with FirstOrDefault in the LINQ lesson.

Combined with what you learned in the previous lesson, an object is saved to and recovered from disk in just two lines:

File.WriteAllText("member.json", JsonSerializer.Serialize(member));
Member? recoveredMember = JsonSerializer.Deserialize<Member>(File.ReadAllText("member.json"));

  1. Customizing the JSON: JsonSerializerOptions and [JsonPropertyName]

By default, JsonSerializer uses the exact name of each C# property as the JSON key ("Name", capitalized). Two mechanisms let you customize this:

  • JsonSerializerOptions: a configuration object passed as a second argument, with options like WriteIndented (formats the JSON with line breaks and indentation, much easier to inspect a file by hand).
  • [JsonPropertyName("key")]: an attribute (attributes are studied in depth in Module 6) placed on a property to indicate which JSON key to use instead, useful when the JSON must follow a different convention than PascalCase (for example, the snake_case common in many APIs).
using System.Text.Json;
using System.Text.Json.Serialization;

class MemberJson
{
    public int Id { get; set; }

    [JsonPropertyName("full_name")]
    public string Name { get; set; } = string.Empty;
}

var options = new JsonSerializerOptions { WriteIndented = true };

MemberJson member = new MemberJson { Id = 1, Name = "Ana Martinez" };
string json = JsonSerializer.Serialize(member, options);

Console.WriteLine(json);
// {
//   "Id": 1,
//   "full_name": "Ana Martinez"
// }

For the rest of this lesson, Member is serialized without [JsonPropertyName] (the JSON keys match the C# properties as-is), but it's worth knowing the attribute because it will reappear in the module's last lesson, when consuming an external REST API whose JSON almost certainly doesn't follow C# naming conventions.

  1. The inheritance challenge: serializing a catalog with Book and Magazine

Library.Catalog is a List<LibraryItem> that actually holds a mix of Book and Magazine (polymorphism, Module 3). Serializing this list as-is produces a problem: when deserializing, JsonSerializer has no way to know whether each element of the JSON should be rebuilt as Book or as MagazineLibraryItem is abstract and can't be instantiated directly.

Since .NET 7, System.Text.Json solves this with polymorphic serialization: the base class is annotated with [JsonPolymorphic] and [JsonDerivedType] for each derived type, indicating a "discriminator" value that's saved alongside each object and lets it reconstruct the correct type when deserializing:

using System.Text.Json.Serialization;

[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(Book), "book")]
[JsonDerivedType(typeof(Magazine), "magazine")]
abstract class LibraryItem : ILendable, ISearchable
{
    // ... Title, Author, Available, constructor, Lend(), Return(), ShowDetails(),
    //     Describe(), Matches() unchanged ...
}

With these attributes added, serializing and deserializing List<LibraryItem> works transparently, automatically including the concrete type of each element:

List<LibraryItem> catalog = new List<LibraryItem>
{
    new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"),
    new Magazine("National Geographic", "Various authors", 302)
};

var options = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(catalog, options);
Console.WriteLine(json);
// [
//   { "type": "book", "Isbn": "978-84-376-0495-4", "Title": "Hopscotch", ... },
//   { "type": "magazine", "IssueNumber": 302, "Title": "National Geographic", ... }
// ]

List<LibraryItem>? recoveredCatalog =
    JsonSerializer.Deserialize<List<LibraryItem>>(json, options);

Console.WriteLine(recoveredCatalog?[0] is Book);      // True
Console.WriteLine(recoveredCatalog?[1] is Magazine);  // True

Without [JsonPolymorphic]/[JsonDerivedType], JsonSerializer.Deserialize<List<LibraryItem>> would throw an exception, because there'd be no way to know which concrete type to instantiate for each element of the JSON array. This same challenge — how to persist a model with inheritance — will reappear, solved a different way, once you reach Entity Framework later in this module.

  1. Persisting all of Library in JSON

With everything above, Library replaces the previous lesson's plain text methods with a JSON-based version, which saves Catalog and Members in a single file using a small helper class that groups both collections:

class LibraryState
{
    public List<LibraryItem> Catalog { get; set; } = new List<LibraryItem>();
    public List<Member> Members { get; set; } = new List<Member>();
}

class Library
{
    // ... Catalog, Members, Loans, _membersById, LoanRegistered unchanged ...
    // ... AddItem, AddMember, FindMemberById, RegisterLoan, LendBookAsync unchanged ...

    private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
    {
        WriteIndented = true
    };

    public void SaveStateJson(string path)
    {
        LibraryState state = new LibraryState
        {
            Catalog = Catalog,
            Members = Members
        };

        string json = JsonSerializer.Serialize(state, JsonOptions);
        File.WriteAllText(path, json);
    }

    public void LoadStateJson(string path)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException($"State file not found: {path}");
        }

        string json = File.ReadAllText(path);
        LibraryState? state = JsonSerializer.Deserialize<LibraryState>(json, JsonOptions);

        if (state is null)
        {
            return;
        }

        Catalog.Clear();
        foreach (LibraryItem item in state.Catalog)
        {
            AddItem(item);
        }

        Members.Clear();
        foreach (Member member in state.Members)
        {
            AddMember(member);
        }
    }
}

LibraryState is a throwaway class, meant exclusively for this purpose: it groups Catalog and Members into a single object so JsonSerializer produces one file with both collections, instead of two separate files. SaveStateJson and LoadStateJson directly replace SaveCatalogText/LoadCatalogText from the previous lesson, with the added benefit that they now also persist Members, something the plain text format didn't cover.

Common Mistakes and Tips

  • Serializing a type with inheritance without [JsonPolymorphic]/[JsonDerivedType]: serializing a List<LibraryItem> "seems" to work (it produces valid JSON), but deserialization fails, because System.Text.Json doesn't know how to reconstruct an abstract type. If the model has inheritance and you need to deserialize it back to its concrete types, these attributes aren't optional.
  • Confusing Serialize/Deserialize with data validation: serialization doesn't validate that the data makes business sense (an Available that should actually be impossible, for example); it only converts the object's state from one format to another. Validation remains the responsibility of the class itself (constructors, properties with a controlled set, as in the Encapsulation lesson).
  • Forgetting that Deserialize<T> can return null: always check the result with is null before using it, just like with any other nullable reference type.
  • Using BinaryFormatter because it shows up in old tutorials: it's obsolete and poses a real security risk when deserializing data you don't fully control; in new code, always use System.Text.Json (or, if a project already used it before, the third-party alternative Newtonsoft.Json, itself now falling out of favor against the standard library).
  • Tip: turn on WriteIndented = true while developing and debugging (the resulting JSON is much easier to inspect at a glance); consider turning it off in very large production files, where the extra space from indentation might matter.

Exercises

  1. Create a Member class (the one you already know) and serialize it to JSON with JsonSerializerOptions { WriteIndented = true }. Show the result on the console and check visually that the keys use the C# property names.

  2. Deserialize the JSON "[{\"Id\":1,\"Name\":\"Ana Martinez\"},{\"Id\":2,\"Name\":\"Luis Gomez\"}]" into a List<Member> and show the Name of each recovered member with a foreach.

  3. Add [JsonPolymorphic]/[JsonDerivedType] to LibraryItem as shown in this lesson. Create a List<LibraryItem> with a Book and a Magazine, serialize it, and deserialize it back into a new List<LibraryItem>; check with is Book/is Magazine that each element recovered its original concrete type.

Solutions

Member member = new Member(1, "Ana Martinez");
var options = new JsonSerializerOptions { WriteIndented = true };

string json = JsonSerializer.Serialize(member, options);
Console.WriteLine(json);
// {
//   "Id": 1,
//   "Name": "Ana Martinez"
// }
string json = "[{\"Id\":1,\"Name\":\"Ana Martinez\"},{\"Id\":2,\"Name\":\"Luis Gomez\"}]";
List<Member>? members = JsonSerializer.Deserialize<List<Member>>(json);

if (members is not null)
{
    foreach (Member member in members)
    {
        Console.WriteLine(member.Name);
    }
}
List<LibraryItem> catalog = new List<LibraryItem>
{
    new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4"),
    new Magazine("National Geographic", "Various authors", 302)
};

var options = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(catalog, options);

List<LibraryItem>? recovered =
    JsonSerializer.Deserialize<List<LibraryItem>>(json, options);

Console.WriteLine(recovered?[0] is Book);     // True
Console.WriteLine(recovered?[1] is Magazine); // True

Conclusion

In this lesson you've learned what serialization is, why JSON has become the standard format over historical alternatives like binary or XML, and how to use System.Text.Json to serialize and deserialize objects and collections, including the more complex case of a model with inheritance via [JsonPolymorphic]/[JsonDerivedType]. Library now persists all of Catalog and Members in a JSON file, with SaveStateJson and LoadStateJson, definitively replacing the previous lesson's manual parsing.

Saving the entire state to a file every time, however, stops being practical once the data grows large, or once several parts of an application need to read and write the same state concurrently and consistently: that's the territory of relational databases. The next lesson, Database Connectivity, introduces classic ADO.NET to save BiblioTech's catalog in a real SQLite database, laying the groundwork for Entity Framework, the lesson right after it.

© Copyright 2026. All rights reserved