C# example

How to parse JSON into an object in C#

8 min read Updated Oct 2026 Runs in an isolated runtime
Quick answer

Declare a record or class that matches the JSON, then call JsonSerializer.Deserialize<T>(json, JsonSerializerOptions.Web) from System.Text.Json. The options matter: by default, property matching is case-sensitive, so a JSON key "name" never fills a property called Name and you get null and 0 back. With no class to deserialize into, use JsonNode.Parse(json) and read values with root["user"]["name"].

Since .NET Core 3.0, JSON support has been built into the framework as System.Text.Json, so you don't need the Newtonsoft.Json package that most older answers use. It gives you three ways in. A typed Deserialize<T> call, where your type is the schema. JsonNode, a mutable tree you index like a dictionary, which is the answer to "deserialize into a dynamic object". And JsonDocument, a fast read-only view. The defaults are stricter than Newtonsoft's, and most "why is everything null?" questions come from that. Each example runs on this page: hit Run, then edit the JSON and run it again. Going the other way? See convert an object to JSON.

1JsonSerializer.Deserialize<T> into a record or classRecommended

Pass the JSON string and the target type. A positional record is the shortest way to declare the shape: System.Text.Json calls its constructor, matching each parameter to a JSON key. The one thing to get right is the options. JsonSerializerOptions.Web (.NET 9+; on older versions use new JsonSerializerOptions(JsonSerializerDefaults.Web)) turns on case-insensitive matching, camelCase names and numbers in quotes, which is what most web APIs send.

Program.cs

Output

Prints Ada Lovelace, 37 and English, French: the values are real string, int and string[] values, and the email key, which User doesn't declare, is skipped without an error. The call with no options prints True and 0: it ran without an exception and filled nothing, because "name" and Name differ in case. The partial object prints Linus, 0, True, since missing keys get the type's default, even for a string[] you declared as non-nullable. The last line is True: Deserialize returns T? because the JSON text null is valid input, so check for null (the ! here just tells the compiler you know it isn't).

2Matching JSON names to C# properties

Real payloads rarely use PascalCase. A naming policy maps every property at once: JsonNamingPolicy.SnakeCaseLower (.NET 8+) reads order_id into OrderId, and there are KebabCaseLower and camelCase policies too. For a single odd key, put [JsonPropertyName("…")] on the property. Two value conversions also need opting in: enums written as strings, and numbers sent in quotes.

Program.cs

Output

Prints 1042, Grace Hopper, True, 39.98 (the quoted "19.99" became a decimal) and Arlington, VA. Without JsonStringEnumConverter, the string fails with The JSON value could not be converted to Status. Path: $.Status | LineNumber: 0 | BytePositionInLine: 19. An enum is read from its number by default, so 2 prints Delivered. You can also put [JsonConverter(typeof(JsonStringEnumConverter<Status>))] on the enum itself so every caller gets string handling. For a class like Order, give non-nullable string properties a default such as = "", because a missing key leaves the initializer's value in place.

3JSON arrays, nested objects and dictionaries

A JSON array maps to List<T> or T[], at the top level or inside an object. A nested object needs a type of its own, and you can nest records as deep as the data goes. When the keys are data rather than field names, such as product codes or dates, deserialize into Dictionary<string, T> instead of a class.

Program.cs

Output

Prints 2 and Linus for the list, 8 for the int[], then Ada, csharp dotnet and Linus #2, Grace #3 from the nested Repo, all with no manual walking of the tree. The dictionary prints apples: 12, pears: 0 and plums: 7. It is sorted with OrderBy before printing because a Dictionary doesn't promise any order. To loop over one, see iterate over a dictionary.

4No class? Use JsonNode as a dynamic object

When the shape varies, or you need two values from a large document, declaring classes is overkill. JsonNode.Parse (.NET 6+, in System.Text.Json.Nodes) gives you a tree you index with strings and numbers, read with GetValue<T>() or a cast, and can change and write back out. It is the System.Text.Json counterpart of Newtonsoft's JObject, and it is what to use where older answers say dynamic.

Program.cs

Output

Prints 7 Ada admin, then (not set): a missing key gives null instead of throwing, so ?. and ?? handle optional paths. The loops print role: admin, role: dev and theme = dark. After the edits, ToJsonString() prints {"id":7,"user":{"name":"Ada","roles":["admin","dev","ops"]},"settings":{"theme":"light"}}. GetValue<T> never converts between JSON types: asking for an int from a string throws InvalidOperationException: An element of type 'String' cannot be converted to a 'System.Int32'. The ! after each index is there because every lookup can return null.

5Read-only access with JsonDocument

JsonDocument parses into a read-only tree of JsonElement values. It allocates less than JsonNode because it rents its memory from a pool, which is also why it is IDisposable: declare it with using. Check ValueKind before reading a value that may be null, and use TryGetProperty for keys that may be missing. A JsonElement is also what you get from Deserialize<object> and Deserialize<dynamic>, as the end of the example shows.

Program.cs

Output

Prints Oslo, then 9:00 4.5, 12:00 7.25 and 15:00 no reading for the reading whose temp is null (calling GetDouble() on it would throw). The optional lookup prints no country, and GetProperty on the same key throws KeyNotFoundException. The last two lines are JsonElement and 'System.Text.Json.JsonElement' does not contain a definition for 'city': unlike Newtonsoft, System.Text.Json has no dynamic support, so d.city compiles and then fails at run time. Use JsonNode or a typed class instead. A JsonElement becomes unusable once its document is disposed; call Clone() on one you need to keep.

6Strict parsing and JsonException

Out of the box, Deserialize is strict about syntax and types but relaxed about shape: unknown keys are ignored and missing ones get defaults. Three opt-ins make it validate the shape too. The C# 11 required modifier makes a key mandatory (.NET 7+). RespectNullableAnnotations (.NET 9+) rejects null for a non-nullable reference type. JsonUnmappedMemberHandling.Disallow (.NET 8+) rejects keys the type doesn't declare. Every failure is a JsonException, and its message says where.

Program.cs

Output

The first input prints ok: Ada, 36. The wrong type prints The JSON value could not be converted to System.Int32. Path: $.age | LineNumber: 0 | BytePositionInLine: 32., and the truncated document fails in the reader with '6' is an invalid end of a number. Expected a delimiter. Path: $.age | LineNumber: 0 | BytePositionInLine: 22. The missing key gives JSON deserialization for type 'Person' was missing required properties including: 'name'., the explicit null gives The property or field 'name' on type 'Person' doesn't allow setting null values. and the extra key gives The JSON property 'admin' could not be mapped to any .NET member contained in type 'Person'. RespectNullableAnnotations reads the compiler's nullable annotations, so it only works in code compiled with nullable reference types on. With the #nullable enable line removed, the null input prints ok: , 36. Catch JsonException around input you don't control.

7Which should you use?

MethodYou getSpeedBest for
JsonSerializer.Deserialize<T>Typed objects, checked by the compilerFastAlmost everything: the default
Deserialize<List<T>> / <Dictionary<string, T>>Typed collectionsFastTop-level arrays, objects keyed by data
JsonNode.ParseMutable tree, indexersSlower, allocates per nodeUnknown or varying shapes, editing JSON
JsonDocument.ParseRead-only JsonElement treeFast, pooled memoryReading a few values from large input
Deserialize<dynamic>A JsonElement, not a dynamic objectFails at run timeAvoid; use JsonNode

Frequently asked questions

Why are all my properties null or 0 after JsonSerializer.Deserialize?

Almost always case: System.Text.Json matches names case-sensitively by default, so "name" does not fill Name, and no exception tells you. Pass JsonSerializerOptions.Web or set PropertyNameCaseInsensitive = true. Two other causes: public fields are ignored unless you set IncludeFields = true, and properties with a private setter are skipped unless you mark them [JsonInclude].

How do I deserialize JSON into a dynamic object in C#?

Use JsonNode.Parse(json) from System.Text.Json.Nodes and read values with indexers, for example (string)root["user"]["name"] or root["id"].GetValue<int>(). JsonSerializer.Deserialize<dynamic> compiles, but it returns a JsonElement, so d.name throws RuntimeBinderException at run time. dynamic with JObject only works with the Newtonsoft.Json package.

What is the System.Text.Json equivalent of JsonConvert.DeserializeObject?

JsonSerializer.Deserialize<T>(json) replaces JsonConvert.DeserializeObject<T>(json), and JsonNode replaces JObject/JToken. The defaults are stricter than Newtonsoft's: names are case-sensitive, a number in quotes like "5" is not read into an int, and comments or trailing commas throw a JsonException. JsonSerializerOptions.Web fixes the first two, and ReadCommentHandling = JsonCommentHandling.Skip plus AllowTrailingCommas = true fix the last.

How do I read a JSON file into an object?

For a small file, JsonSerializer.Deserialize<List<User>>(File.ReadAllText("users.json"), JsonSerializerOptions.Web). For a large file, deserialize from the stream so the whole text never sits in memory as one string: await using FileStream fs = File.OpenRead("users.json"); then await JsonSerializer.DeserializeAsync<List<User>>(fs, JsonSerializerOptions.Web). For an HTTP response, HttpClient.GetFromJsonAsync<T>(url) from System.Net.Http.Json does the request and the deserializing, and it uses the web defaults.

What do I get from Deserialize<Dictionary<string, object>>?

The keys are strings, but every value is a JsonElement, not an int or a string: dict["a"].GetType().Name is JsonElement. You still have to call GetInt32(), GetString() and so on. If the values all have one type, use Dictionary<string, int> (or that type); if they vary, JsonNode is easier to work with.

Does System.Text.Json work with records and init-only properties?

Yes. A positional record is deserialized through its constructor, matching each parameter to a JSON key by name (case-insensitively under JsonSerializerOptions.Web), and init properties are set like normal ones. Combine init with required when a key must be present: a missing one then throws JsonException instead of leaving a default.

Run it yourself

Open any of these in the full C# editor to paste your own JSON, run it and share.

C# playground