Every time an app talks to a web API, there's a very good chance the words passing between them are JSON.
Your C# program has an Order object — a real, in-memory .NET object with properties, methods, and a specific type. A web API written in a completely different language, running on a completely different server, needs to receive that same order's data. C# objects can't travel across a network — but text can. JSON is the text format almost every modern system has agreed to use for exactly this handoff.
In this lesson, you'll learn what JSON is, why it became the near-universal format for this kind of data exchange, and how to serialize and deserialize C# objects to and from JSON using System.Text.Json — .NET's modern, built-in JSON library.
JSON (JavaScript Object Notation) is a plain-text way of writing structured data — objects, lists, numbers, strings, booleans — using a small, simple, human-readable syntax. Despite the name, it's not tied to JavaScript at all anymore; it's a language-neutral text format that virtually every modern programming language, including C#, can read and write.
{
"orderId": 1042,
"customerName": "Acme Co",
"total": 149.99,
"isPaid": true,
"items": ["Widget", "Gadget"]
}JSON is a lightweight, text-based data-interchange format built on two structures: an object — an unordered collection of key/value pairs, written with { } — and an array — an ordered list of values, written with [ ]. Values can be strings, numbers, booleans, null, or nested objects/arrays. System.Text.Json is the JSON library built into .NET since .NET Core 3.0, and it's the idiomatic, default choice for JSON work in modern .NET — it converts .NET objects to JSON text (serialization) and JSON text back into .NET objects (deserialization).
An in-memory C# object is specific to the .NET runtime — its exact bit layout, its type metadata, all of it is meaningless to a Python service, a JavaScript browser, or a mobile app written in Swift. If two systems need to exchange structured data and they're not both running the exact same runtime, they need a shared, neutral format both sides already know how to read and write.
The industry needed a data format that's language-neutral, simple enough to read by eye when debugging, compact enough to send efficiently over a network, and expressive enough to represent the nested objects and lists real applications actually use.
JSON satisfies all of that, and virtually every language — C#, Python, JavaScript, Java, Go, Swift — has mature, standard library support for it. This is precisely why it became the default format for REST APIs, configuration files, message queues, and countless other places where two independent pieces of software need to agree on structured data.
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = "";
public decimal Total { get; set; }
public bool IsPaid { get; set; }
}
var order = new Order { Id = 1042, CustomerName = "Acme Co", Total = 149.99m, IsPaid = true };
string json = JsonSerializer.Serialize(order);
// {"Id":1042,"CustomerName":"Acme Co","Total":149.99,"IsPaid":true}
string incomingJson = "{\"Id\":2001,\"CustomerName\":\"Globex\",\"Total\":89.5,\"IsPaid\":false}";
Order? parsed = JsonSerializer.Deserialize<Order>(incomingJson);
Console.WriteLine(parsed?.CustomerName); // "Globex"
Deserialize<T> returns a nullable reference — it's null if the JSON literally represents the value null, which is why the result is typically checked before use.JsonSerializerOptions controls how serialization behaves — two of the most commonly reached-for options:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // "CustomerName" → "customerName"
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // skip null properties entirely
WriteIndented = true // pretty-print with line breaks and indentation, easier to read/debug
};
string json = JsonSerializer.Serialize(order, options);
// {
// "id": 1042,
// "customerName": "Acme Co",
// "total": 149.99,
// "isPaid": true
// }camelCase naming matters a great deal in practice — most JSON APIs (especially anything with a JavaScript front end) expect property names like customerName, not the C# convention of CustomerName. Applying JsonNamingPolicy.CamelCase lets your C# class keep normal C# naming conventions while still producing (and accepting) JSON in the style the rest of the ecosystem expects.
Round-tripping a simple object, and also reading/writing JSON directly to a file (tying this back to the File lessons earlier in the module):
public class Product
{
public string Sku { get; set; } = "";
public string Name { get; set; } = "";
public decimal Price { get; set; }
public string? DiscontinuedReason { get; set; } // null unless discontinued
}
var product = new Product { Sku = "WGT-01", Name = "Widget", Price = 12.50m, DiscontinuedReason = null };
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string json = JsonSerializer.Serialize(product, options);
Console.WriteLine(json);
// {"sku":"WGT-01","name":"Widget","price":12.5}
// Note: discontinuedReason is entirely absent — it was null, and we asked to skip nulls
await File.WriteAllTextAsync("product.json", json);
// ─── Later, read it back ───
string savedJson = await File.ReadAllTextAsync("product.json");
Product? reloaded = JsonSerializer.Deserialize<Product>(savedJson, options);
Console.WriteLine(reloaded?.Name); // "Widget"A REST API endpoint receiving a new order as JSON and responding with a JSON confirmation — the everyday shape of nearly every modern web API request/response cycle:
public record CreateOrderRequest(string CustomerName, List<string> ItemSkus);
public record OrderConfirmation(int OrderId, string Status, DateTimeOffset ConfirmedAtUtc);
public class OrderApiHandler
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Simulates receiving a raw JSON request body, as a web framework would hand it to you
public string HandleCreateOrder(string requestBodyJson)
{
CreateOrderRequest? request = JsonSerializer.Deserialize<CreateOrderRequest>(requestBodyJson, JsonOptions);
if (request is null || request.ItemSkus.Count == 0)
{
var error = new { error = "Request must include at least one item." };
return JsonSerializer.Serialize(error, JsonOptions);
}
int newOrderId = SaveOrder(request);
var confirmation = new OrderConfirmation(newOrderId, "Confirmed", DateTimeOffset.UtcNow);
return JsonSerializer.Serialize(confirmation, JsonOptions);
}
}
// ─── Simulated incoming request ───
string incoming = """{"customerName":"Acme Co","itemSkus":["WGT-01","GDG-02"]}""";
var handler = new OrderApiHandler();
Console.WriteLine(handler.HandleCreateOrder(incoming));
// {"orderId":8842,"status":"Confirmed","confirmedAtUtc":"2026-08-29T18:42:11.0000000+00:00"}This is essentially what happens under the hood every time you call a modern web API — a JSON request body comes in, gets deserialized into a strongly typed C# object you can work with normally, and the response goes back out the same way. Note also that JsonSerializer.Serialize works fine on record types and even anonymous objects (new { error = "..." }) — it isn't limited to plain classes.
At a first-look level, it's enough to know that System.Text.Json works by reflecting over your class's public properties (or using source-generated metadata in performance-sensitive scenarios) to figure out how to map each JSON key to a matching .NET property, and vice versa in the other direction. It matches property names against JSON keys case-insensitively during deserialization by default, which is why "customerName" in the JSON still correctly maps to a C# property named CustomerName even without an explicit naming policy configured for that direction.
System.Text.Json is Microsoft's modern, built-in replacement — it ships as part of the runtime itself (no extra package needed), and is the current, idiomatic default for new .NET code.
The name is a historical artifact — JSON originated from JavaScript's own object literal syntax, but it long ago became a fully language-independent standard. A C# service, a Python script, and a Java backend can all read and write the exact same JSON with no special JavaScript involved anywhere.
It's easy to mix these up early on: Serialize takes a .NET object and turns it into JSON text (object → string). Deserialize takes JSON text and turns it into a .NET object (string → object). A useful memory hook: "serial" as in a series of characters — serializing produces text.
Wrong:
Order order = JsonSerializer.Deserialize<Order>(json)!; // "!" silences the warning, doesn't remove the risk
Console.WriteLine(order.CustomerName); // could throw NullReferenceException if json was literally "null"Correct:
Order? order = JsonSerializer.Deserialize<Order>(json);
if (order is null)
{
Console.WriteLine("Received empty or null order data.");
return;
}
Console.WriteLine(order.CustomerName); Deserializing user-supplied or externally-received JSON without a try/catch. Malformed JSON throws a JsonException — this is exactly the same "boundary with the outside world" reasoning from the file-handling lessons: JSON coming from outside your program deserves the same defensive handling as a file you didn't create yourself.
try
{
var order = JsonSerializer.Deserialize<Order>(externalJson);
}
catch (JsonException ex)
{
Console.WriteLine($"Received malformed JSON: {ex.Message}");
} Serializing with default settings (PascalCase property names) and sending the result to a JavaScript front end that expects camelCase — the front end's code silently receives undefined for every field because the keys don't match what it's looking for. Set PropertyNamingPolicy = JsonNamingPolicy.CamelCase when the consumer expects it — which, for most public-facing web APIs, is the common default expectation.
System.Text.Json is .NET's modern, built-in JSON library — JsonSerializer.Serialize and Deserialize<T> are the two core operations.JsonSerializerOptions customizes behavior — camelCase naming and skipping nulls are two of the most commonly used settings.You've seen why JSON is so widely used and how to serialize and deserialize with System.Text.Json. Let's check your understanding.
1. Why did JSON become the near-universal format for exchanging data between systems written in different programming languages?
Correct: B
Why B is correct: JSON's appeal is exactly its simplicity and language neutrality — it's plain text with a small, well-defined syntax, and virtually every modern language has built-in or widely used library support for it, making it a natural common ground between systems written in different languages.
Why A is incorrect: Despite its name's origin, JSON is not JavaScript-specific — C#, Python, Java, and many other languages produce and consume it natively, with no JavaScript engine involved.
Why C is incorrect: Many formats can represent numbers; JSON's popularity isn't about unique numeric precision — other formats like XML or binary formats represent numbers too.
Why D is incorrect: JSON itself has no built-in encryption — it's plain readable text; security, if needed, is handled separately (e.g., via HTTPS).
Reinforcement: JSON's simplicity and broad language support are exactly what made it the default choice for cross-system data exchange.
2. What is the difference between JsonSerializer.Serialize and JsonSerializer.Deserialize?
Correct: A
Why A is correct: Serialize takes a .NET object and produces a JSON string representation of it. Deserialize takes a JSON string and reconstructs it as a .NET object of the specified type — they're inverse operations of each other.
Why B is incorrect: Neither operation is inherently tied to file I/O — both work purely with in-memory strings; you can optionally combine them with File.ReadAllText/WriteAllText, but that's a separate step.
Why C is incorrect: They perform opposite conversions — mixing them up would mean trying to "serialize" a JSON string (which doesn't match the method's expected input type) or "deserialize" an object.
Why D is incorrect: Both methods work with full object graphs — classes, records, lists, nested objects — not just strings or just numbers.
Reinforcement: Serialize goes object → text; Deserialize goes text → object. They're direct opposites.
3. A C# class has a property named CustomerName. After serializing with PropertyNamingPolicy = JsonNamingPolicy.CamelCase, what will the resulting JSON key look like?
Correct: C
Why C is correct: JsonNamingPolicy.CamelCase converts PascalCase C# property names to camelCase JSON keys — lowercasing the first letter while preserving the capitalization boundary of subsequent words, exactly matching the convention most JavaScript-facing APIs expect.
Why A is incorrect: The naming policy specifically exists to change how property names are rendered in the output JSON — it has a real, visible effect.
Why B is incorrect: CamelCase preserves word boundaries via capitalization — it doesn't collapse everything to lowercase with no distinction.
Why D is incorrect: snake_case with underscores is a different naming convention entirely — JsonNamingPolicy.CamelCase specifically produces camelCase, not snake_case.
Reinforcement: CamelCase naming policy converts PascalCase to camelCase — lowercase first letter, capital letters at subsequent word boundaries.
4. Why should code that deserializes JSON received from an external source (like an API request) generally be wrapped in a try/catch for JsonException?
Correct: B
Why B is correct: Just like a file you didn't create yourself, JSON arriving from outside your program (a request body, an external API response) isn't guaranteed to be well-formed. If it isn't, Deserialize throws a JsonException — handling that defensively is the same "boundary with the outside world" reasoning applied earlier to file I/O.
Why A is incorrect: Deserialize only throws when something actually goes wrong (malformed JSON, type mismatches) — it doesn't throw on valid, well-formed JSON.
Why C is incorrect: Nullable return types don't require try/catch on their own — that's an unrelated language feature; the JsonException concern is specifically about malformed input.
Why D is incorrect: JsonException is specifically thrown during parsing/deserialization of malformed JSON text — it's the deserialization direction that's primarily at risk from untrusted input.
Reinforcement: Data crossing a trust boundary — whether a file or JSON from an external source — deserves defensive error handling.
5. A team serializes a Product object with a null DiscontinuedReason property using default JsonSerializerOptions (no ignore-null setting). What does the resulting JSON look like for that property?
Correct: B
Why B is correct: Without DefaultIgnoreCondition set to skip nulls, System.Text.Json includes every property in the output by default, representing a null value as the JSON literal null. Omitting null properties entirely requires explicitly opting in with DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, as shown in the lesson's example.
Why A is incorrect: That behavior only happens when the ignore-null option is explicitly configured — it isn't the default behavior.
Why C is incorrect: Serializing a null property value is a perfectly normal, supported operation — it doesn't throw.
Why D is incorrect: System.Text.Json doesn't silently substitute an empty string for null — it faithfully represents null as JSON's null literal unless told otherwise.
Reinforcement: Default behavior includes null properties as JSON null — skipping them is an explicit opt-in via JsonSerializerOptions.
You can now serialize and deserialize C# objects to and from JSON with real confidence. Next, and finally in this module: a broader look at serialization itself — what it means beyond just JSON, and how it compares to XML and binary formats.
dotnetmadeeasy.com — Learn C# and .NET, the right way.