Json Processing
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.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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.
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:
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.
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.
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.
Now you try. Deserialize the JSON into a User , then read a property off the object you built. Fill in the two ___ blanks:
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 .
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.
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 JsonElement s 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.
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:
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.
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] .
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.
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.
Practice quiz
What does JsonSerializer.Serialize(obj) do?
- Turns a JSON string into a C# object
- Validates a JSON schema
- Turns a C# object into a JSON string
- Reads JSON from a file
Answer: Turns a C# object into a JSON string. Serialize turns a live C# object into a JSON string; Deserialize is the reverse direction.
By default, is System.Text.Json case-sensitive when matching JSON keys to properties?
- Yes, it is case-sensitive by default
- No, it ignores case by default
- Only for arrays
- Only when WriteIndented is true
Answer: Yes, it is case-sensitive by default. STJ is case-sensitive by default, so JSON key 'city' won't fill property City unless you set PropertyNameCaseInsensitive = true.
What option produces pretty, multi-line JSON output?
- PropertyNameCaseInsensitive = true
- IncludeFields = true
- CamelCase = true
- WriteIndented = true
Answer: WriteIndented = true. WriteIndented = true on JsonSerializerOptions makes the output human-readable with indentation.
What is the return type of JsonSerializer.Deserialize<Weather>(json)?
- Weather (never null)
- Weather? (nullable, because the JSON could be null)
- string
- object
Answer: Weather? (nullable, because the JSON could be null). Deserialize returns a nullable type (Weather?) because the JSON could literally be the text null, so you should null-check the result.
What does the [JsonPropertyName("first_name")] attribute do?
- Maps the exact JSON key 'first_name' to a C# property, both reading and writing
- Hides the property from JSON
- Renames the C# class
- Forces the property to be a string
Answer: Maps the exact JSON key 'first_name' to a C# property, both reading and writing. [JsonPropertyName] maps a specific JSON key (like snake_case 'first_name') to a tidy C# property in both directions.
What does the [JsonIgnore] attribute do?
- Includes private fields
- Encrypts the property
- Leaves the property out of the JSON entirely
- Forces camelCase naming
Answer: Leaves the property out of the JSON entirely. [JsonIgnore] excludes a property from the JSON — ideal for secrets or computed values you never want serialized.
Which tool reads JSON without a model class, read-only and low-allocation?
- JsonNode
- JsonDocument
- JsonSerializer
- Utf8JsonWriter
Answer: JsonDocument. JsonDocument parses JSON into a read-only tree of JsonElements with few allocations; wrap it in using to return its pooled memory.
Which tool lets you read AND edit dynamic JSON without a model class?
- JsonDocument
- JsonSerializer
- JsonElement
- JsonNode
Answer: JsonNode. JsonNode is the read-write cousin of JsonDocument: parse it, then change values or add keys with indexer syntax like node["stars"] = 9999.
Why should you create JsonSerializerOptions once and reuse it?
- It is required by the compiler
- STJ caches type metadata on the instance, so a new one per call throws that cache away
- It changes the JSON output
- It enables async serialization
Answer: STJ caches type metadata on the instance, so a new one per call throws that cache away. STJ caches type metadata against the options instance; creating a fresh options object on every call discards that cache and slows you down.
What does System.Text.Json require to deserialize into a class by default?
- A constructor that takes all properties
- Private fields only
- A public parameterless constructor and public settable properties
Answer: A public parameterless constructor and public settable properties. By default STJ builds the object with a public parameterless constructor and sets public properties; it ignores private members unless you opt in.