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
- What serialization is and what problem it solves
- A look at the past: why JSON won out over binary and XML
System.Text.Json: serializing withJsonSerializer.Serialize- Deserializing with
JsonSerializer.Deserialize - Customizing the JSON:
JsonSerializerOptionsand[JsonPropertyName] - The inheritance challenge: serializing a catalog with
BookandMagazine - Persisting all of
Libraryin JSON
- 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]
- 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.
System.Text.Json: serializing with JsonSerializer.Serialize
System.Text.Json: serializing with JsonSerializer.SerializeThe 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"}]
- Deserializing with
JsonSerializer.Deserialize
JsonSerializer.DeserializeThe 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); // 2Deserialize<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"));
- Customizing the JSON:
JsonSerializerOptions and [JsonPropertyName]
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 likeWriteIndented(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, thesnake_casecommon 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.
- The inheritance challenge: serializing a catalog with
Book and Magazine
Book and MagazineLibrary.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 Magazine — LibraryItem 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); // TrueWithout [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.
- Persisting all of
Library in JSON
Library in JSONWith 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 aList<LibraryItem>"seems" to work (it produces valid JSON), but deserialization fails, becauseSystem.Text.Jsondoesn't know how to reconstruct anabstracttype. If the model has inheritance and you need to deserialize it back to its concrete types, these attributes aren't optional. - Confusing
Serialize/Deserializewith data validation: serialization doesn't validate that the data makes business sense (anAvailablethat 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 controlledset, as in the Encapsulation lesson). - Forgetting that
Deserialize<T>can returnnull: always check the result withis nullbefore using it, just like with any other nullable reference type. - Using
BinaryFormatterbecause 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 useSystem.Text.Json(or, if a project already used it before, the third-party alternativeNewtonsoft.Json, itself now falling out of favor against the standard library). - Tip: turn on
WriteIndented = truewhile 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
-
Create a
Memberclass (the one you already know) and serialize it to JSON withJsonSerializerOptions { WriteIndented = true }. Show the result on the console and check visually that the keys use the C# property names. -
Deserialize the JSON
"[{\"Id\":1,\"Name\":\"Ana Martinez\"},{\"Id\":2,\"Name\":\"Luis Gomez\"}]"into aList<Member>and show theNameof each recovered member with aforeach. -
Add
[JsonPolymorphic]/[JsonDerivedType]toLibraryItemas shown in this lesson. Create aList<LibraryItem>with aBookand aMagazine, serialize it, and deserialize it back into a newList<LibraryItem>; check withis Book/is Magazinethat 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.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
