Skip to main content
    Courses/C#/JSON Processing

    Lesson 30 • Advanced Track

    JSON Processing with System.Text.Json

    By the end of this lesson you'll be able to turn C# objects into JSON and back again with .NET's built-in System.Text.Json — controlling the output format, mapping awkward API key names onto tidy C# properties, and reading JSON you don't even have a class for. This is the skill behind every API call, config file, and saved document your apps will touch.

    What You'll Learn

    • Serialize C# objects to JSON with JsonSerializer.Serialize
    • Deserialize JSON back into typed objects with Deserialize<T>
    • Format output with JsonSerializerOptions (WriteIndented, camelCase)
    • Match mismatched casing with PropertyNameCaseInsensitive
    • Map awkward keys with [JsonPropertyName] and hide fields with [JsonIgnore]
    • Read schema-less JSON with JsonDocument and edit it with JsonNode

    💡 Real-World Analogy

    JSON serialization is like flat-pack furniture. Your C# object is an assembled chair — fine to use in your living room (your program), but impossible to post through a letterbox. Serializing flattens it into a labelled, boxed kit (a JSON string) that travels easily across a network or onto disk. Deserializing is the person at the other end following the labels to rebuild the exact same chair. System.Text.Json is the factory that packs and unpacks — and because it's built into .NET, there's nothing extra to install and it's fast enough for high-traffic servers.

    The JSON Toolbox at a Glance

    JSON (JavaScript Object Notation) is a plain-text format for structured data: objects in {curly braces}, arrays in [square brackets], plus strings, numbers, true/false and null. It's the universal language of web APIs. System.Text.Json (often shortened to STJ) is the modern, high-performance JSON library that ships inside .NET — no NuGet package required.

    It gives you a few tools for different jobs. Pick by how much structure you know up front:

    ToolBest forRead / Write
    JsonSerializerKnown shapes — DTOs, API models, configBoth
    JsonDocumentPeeking at unknown JSON, read-only, fastRead only
    JsonNodeEditing dynamic JSON without a classBoth
    Utf8JsonWriterBuilding JSON by hand at top speedWrite only

    Ninety percent of the time you'll reach for JsonSerializer — so that's where we start, and where you'll spend most of this lesson.

    1. Serializing — Object → JSON

    Serializing means turning a live C# object into a JSON string you can send or save. The one call you need is JsonSerializer.Serialize(obj). By default it reads every public property and produces compact, single-line JSON with PascalCase keys. To shape the output you pass a JsonSerializerOptions: WriteIndented = true for readable formatting, and PropertyNamingPolicy = JsonNamingPolicy.CamelCase to emit the camelCase keys that most web APIs expect. Read this worked example, run it, then you'll serialize one yourself.

    Worked example: serialize an object

    Read every comment, run it, and compare compact vs indented output.

    Try it Yourself »
    C#
    using System;
    using System.Text.Json;
    
    // A plain class (a DTO — "data transfer object") that mirrors your JSON shape.
    class Book
    {
        public string Title { get; set; } = "";
        public string Author { get; set; } = "";
        public int Year { get; set; }
        public bool InStock { get; set; }
    }
    
    class Program
    {
        static void Main()
        {
            // 1) Build a normal C# object.
            var book = new Book
            {
                Title = "The Pragmatic Programmer",
                Author = "Hunt & Thom
    ...

    Your turn. The program below is almost complete — fill in the two blanks marked ___ using the hints in the comments, then run it and check the indented output.

    🎯 Your turn: serialize a Movie

    Turn on indenting and call the Serialize method, then check the output.

    Try it Yourself »
    C#
    using System;
    using System.Text.Json;
    
    class Movie
    {
        public string Title { get; set; } = "";
        public int Year { get; set; }
    }
    
    class Program
    {
        static void Main()
        {
            // 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
    
            // 1) Build a Movie object.
            var movie = new Movie { Title = "Inception", Year = 2010 };
    
            // 2) Make options that pretty-print the JSON.
            var options = new JsonSerializerOptions
            {
                WriteIndented = _
    ...

    2. Deserializing — JSON → Object

    Deserializing is the reverse: JsonSerializer.Deserialize<T>(json) reads a JSON string and rebuilds a typed C# object. You name the target type in the angle brackets — Deserialize<Weather>(json) — and STJ creates the object and copies each matching value into a property. Two things bite beginners here. First, the result is nullable (Weather?), because the JSON could literally be null. Second, STJ is case-sensitive by default: a JSON key "city" will not fill a C# property City unless you set PropertyNameCaseInsensitive = true. Watch both in the worked example.

    Worked example: deserialize into an object

    See case-insensitive matching and safe null handling in action.

    Try it Yourself »
    C#
    using System;
    using System.Text.Json;
    
    class Weather
    {
        // System.Text.Json needs a PUBLIC parameterless constructor (the
        // default one you get for free) so it can build the object, then
        // fill these settable properties one by one.
        public string City { get; set; } = "";
        public double TempC { get; set; }
        public bool IsRaining { get; set; }
    }
    
    class Program
    {
        static void Main()
        {
            // The JSON you might receive from a weather API.
            string json = "{\"ci
    ...

    Now you try. Deserialize the JSON into a User, then read a property off the object you built. Fill in the two ___ blanks:

    🎯 Your turn: deserialize then read a property

    Name the type in the angle brackets, then print the Name property.

    Try it Yourself »
    C#
    using System;
    using System.Text.Json;
    
    class User
    {
        public string Name { get; set; } = "";
        public int Age { get; set; }
    }
    
    class Program
    {
        static void Main()
        {
            // 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
    
            string json = "{\"name\":\"Ada\",\"age\":36}";
    
            // 1) Deserialize the JSON into a User.
            //    The type goes in the angle brackets:  Deserialize<User>(...)
            User? user = JsonSerializer.Deserialize<___>(json);  // 👉 the 
    ...

    3. Mapping Names with Attributes

    Real APIs rarely hand you keys that match your C# property names. A field might arrive as "first_name" — which isn't even a legal C# identifier. Decorate the property with [JsonPropertyName("first_name")] and STJ maps that exact key to your tidy FirstName property, both when reading and writing. Use [JsonIgnore] on anything you never want in the JSON — a password, a session token, or a value you compute on the fly. These attributes live in the System.Text.Json.Serialization namespace, so remember the second using.

    Worked example: [JsonPropertyName] & [JsonIgnore]

    Map snake_case keys to PascalCase properties and hide a secret field.

    Try it Yourself »
    C#
    using System;
    using System.Text.Json;
    using System.Text.Json.Serialization;
    
    // Real APIs love snake_case keys ("first_name") that aren't valid C#
    // identifiers. [JsonPropertyName] maps the exact JSON key to a tidy
    // C# property name — both directions, serialize AND deserialize.
    class Account
    {
        [JsonPropertyName("first_name")]
        public string FirstName { get; set; } = "";
    
        [JsonPropertyName("last_name")]
        public string LastName { get; set; } = "";
    
        // [JsonIgnore] leaves this 
    ...

    🔎 Deep Dive: the JsonSerializerOptions object

    Nearly every knob you'll ever turn lives on one object. Build it once and reuse it — STJ caches type metadata against the options instance, so creating a fresh one on every call quietly throws that cache away and slows you down.

    var options = new JsonSerializerOptions
    {
        WriteIndented = true,                          // pretty, multi-line output
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Title -> "title"
        PropertyNameCaseInsensitive = true,            // match keys ignoring case
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // skip nulls
    };
    
    // Reuse it for every call:
    string json = JsonSerializer.Serialize(obj, options);
    var back   = JsonSerializer.Deserialize<MyType>(json, options);

    A naming policy (CamelCase) changes all properties at once; a [JsonPropertyName] attribute overrides the policy for one specific property. Attribute wins where the two disagree.

    4. JSON Without a Class — JsonDocument & JsonNode

    Sometimes you don't have a model — you just need one value out of a big response, or you want to tweak a config file you didn't design. JsonDocument parses JSON into a read-only tree of JsonElements with very few allocations; wrap it in using so its pooled memory is returned. JsonNode is the read-write cousin: parse it, then change values or add keys with simple indexer syntax (node["stars"] = 9999;) before turning it back into a string. Reach for these when the shape is unknown or you only care about a slice of it.

    Worked example: JsonDocument & JsonNode

    Read a few values with JsonDocument, then edit a tree with JsonNode.

    Try it Yourself »
    C#
    using System;
    using System.Text.Json;
    using System.Text.Json.Nodes;
    
    class Program
    {
        static void Main()
        {
            string json = """
            {
                "name": "Repo",
                "stars": 1280,
                "tags": ["csharp", "json", "dotnet"]
            }
            """;
    
            // === JsonDocument — read-only, very low allocation ===
            // Great when you only need to PEEK at a few values and won't
            // change anything. 'using' disposes its pooled memory afterwards.
            usin
    ...

    🔎 System.Text.Json vs Newtonsoft.Json

    For years the default JSON library in .NET was Newtonsoft.Json (the Newtonsoft.Json NuGet package, also called Json.NET). You'll still meet it everywhere in existing code, and it's superb — extremely flexible, with features STJ doesn't have.

    System.Text.Json is Microsoft's newer, built-in replacement (since .NET Core 3.0). It needs no package, is noticeably faster, uses less memory, and works with ahead-of-time (AOT) compilation. The trade-offs to remember:

    • • STJ is case-sensitive by default; Newtonsoft is case-insensitive by default. This is the #1 surprise when porting code.
    • • Newtonsoft serializes private members and uses non-public constructors more readily; STJ sticks to public properties and a public parameterless constructor unless you opt in.
    • • Some advanced Newtonsoft features (e.g. JsonConvert settings, certain attributes, LINQ to JSON niceties) have different or no direct equivalents in STJ.

    Rule of thumb: choose System.Text.Json for new projects for the speed and zero dependencies; keep Newtonsoft when you need a feature STJ lacks or you're maintaining code already built on it. The API names rhyme (JsonSerializer.Serialize vs JsonConvert.SerializeObject), so the mental model transfers.

    Pro Tips

    • 💡 Create your JsonSerializerOptions once and reuse it. STJ caches metadata on the instance; a new options object per call silently kills that cache.
    • 💡 Dispose JsonDocument with a using — it rents memory from a pool and leaks it if you don't.
    • 💡 Match the API's casing once with a naming policy rather than tagging every property; reserve [JsonPropertyName] for the odd key that breaks the pattern.
    • 💡 Use JsonSerializer.Serialize(stream, obj) to write straight to a file or response stream — no giant intermediate string in memory.
    • 💡 In .NET 8+, source generators ([JsonSerializable] + JsonSerializerContext) give reflection-free, AOT-friendly serialization — ideal for trimmed or Native AOT apps.

    Common Errors (and the fix)

    • All my properties are empty / 0 after deserializing: a casing mismatch. The JSON key "city" won't fill City by default. Set PropertyNameCaseInsensitive = true, or apply a camelCase naming policy, or use [JsonPropertyName("city")].
    • "JsonException: The JSON value could not be converted to … no parameterless constructor": STJ builds your object with the public, parameterless constructor and then sets properties. Add an empty constructor (or remove the one that takes arguments), or use [JsonConstructor] to point STJ at the right one.
    • Deserialize returns null with no error: the input was literally the text null, or didn't match your type at all. Always treat the result as nullable (MyType?) and check before using it.
    • "System.Text.Json.JsonException: '<char>' is an invalid start of a value": the string isn't valid JSON — a trailing comma, a single quote instead of double, or HTML/an error page where you expected JSON. Print the raw input and validate it.
    • "NullReferenceException" right after deserializing a collection: deserializing missing JSON gives you a null list, not an empty one. Guard with ?? new List<T>() before you loop.

    📋 Quick Reference

    TaskCodeNotes
    Object → JSONJsonSerializer.Serialize(obj)Compact by default
    JSON → objectJsonSerializer.Deserialize<T>(json)Result is T?
    Pretty-printWriteIndented = trueOn the options
    camelCase keysPropertyNamingPolicy = JsonNamingPolicy.CamelCaseAll properties
    Ignore casingPropertyNameCaseInsensitive = trueWhen reading
    Rename one key[JsonPropertyName("k")]Per property
    Skip a property[JsonIgnore]Both directions
    Peek at unknown JSONJsonDocument.Parse(json)Read-only, dispose it
    Edit dynamic JSONJsonNode.Parse(json)Read & write

    Frequently Asked Questions

    Q: My object deserialized but every field is empty — why?

    Almost always a casing mismatch. System.Text.Json is case-sensitive by default, so JSON key "name" doesn't fill property Name. Set PropertyNameCaseInsensitive = true in your options, or use a camelCase naming policy, or tag the property with [JsonPropertyName].

    Q: Do I have to create a class to read JSON?

    No. Use JsonDocument to read values out of arbitrary JSON, or JsonNode if you also want to edit it. A class is just the most convenient option when you know the shape ahead of time.

    Q: Should I use System.Text.Json or Newtonsoft.Json?

    For new code, prefer System.Text.Json — it's built in, faster, and AOT-friendly. Stay with Newtonsoft if you need a feature it lacks or you're maintaining a codebase already built on it. Remember STJ is case-sensitive by default and Newtonsoft isn't.

    Q: Why does STJ ignore my fields and private properties?

    By default it only serializes public properties. To include public fields, set IncludeFields = true in the options (or add [JsonInclude]). It also needs a public parameterless constructor to deserialize.

    Q: How do I serialize an enum as its name instead of a number?

    Add a JsonStringEnumConverter to options.Converters, or tag the property with [JsonConverter(typeof(JsonStringEnumConverter))]. By default enums are written as their underlying integer.

    Mini-Challenge: Round-Trip a List

    No blanks this time — just a brief and an outline to keep you on track. Build a small TodoItem class, make a List<TodoItem>, serialize it to JSON, deserialize that JSON straight back into a fresh list, and print the first item's title. A clean round-trip — out to JSON and back, with nothing lost — is the everyday proof your model and your JSON agree. Run it and check your output against the expected line in the comments.

    🎯 Mini-Challenge: round-trip a list of tasks

    Serialize a List<TodoItem>, deserialize it back, and print the first title.

    Try it Yourself »
    C#
    using System;
    using System.Collections.Generic;
    using System.Text.Json;
    
    // 🎯 MINI-CHALLENGE: Round-trip a list of tasks
    // 1. Define a class TodoItem with:  string Title  and  bool Done  (both get; set;).
    // 2. Build a List<TodoItem> with at least two items.
    // 3. Serialize the list to a JSON string (use WriteIndented = true).
    // 4. Deserialize that JSON string back into a new List<TodoItem>.
    // 5. Print the Title of the FIRST item from the deserialized list.
    //
    // Hints:
    //   - JsonSerializer
    ...

    🎉 Lesson Complete

    • JsonSerializer.Serialize(obj) turns an object into a JSON string
    • JsonSerializer.Deserialize<T>(json) rebuilds a typed object (result is nullable)
    • JsonSerializerOptions controls format: WriteIndented, camelCase policy, case-insensitivity
    • ✅ STJ is case-sensitive by default — the cause of most "empty object" surprises
    • [JsonPropertyName] renames a key; [JsonIgnore] drops a property
    • JsonDocument (read-only) and JsonNode (read-write) handle JSON with no class
    • ✅ Prefer System.Text.Json for new code; Newtonsoft.Json remains widely used
    • Next lesson: REST APIs with ASP.NET Core — where this JSON skill powers every request and response

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service