Mastering JSON for .NET: A Practical Guide for Developers

Getting Started with JSON for .NET: Serialization, Deserialization, and TipsJSON (JavaScript Object Notation) is the de facto standard for data interchange in web APIs, configuration files, and many modern applications. In the .NET ecosystem you have several solid options for working with JSON—primarily System.Text.Json (built into .NET since .NET Core 3.0 and improved since) and Newtonsoft.Json (Json.NET), a mature, feature-rich library. This article covers essentials: choosing a library, performing serialization and deserialization, configuring behavior, handling advanced scenarios, and practical tips to avoid common pitfalls.


Why JSON in .NET matters

  • Interoperability: JSON is language-agnostic and widely supported across clients and services.
  • Performance: Modern .NET JSON libraries are optimized for speed and low allocation.
  • Readability: JSON’s human-readable format simplifies debugging and configuration.

Choosing a library: System.Text.Json vs Newtonsoft.Json

Both libraries are capable; the best choice depends on requirements.

Feature / Need System.Text.Json Newtonsoft.Json (Json.NET)
Included in framework Yes (built-in) No (external package)
Ease of use / maturity Good, improving Very mature, feature-rich
Performance Generally faster, lower allocations Slower in some scenarios
Flexible converters/customization Improving (custom converters) Extensive customization & converters
Polymorphic deserialization Limited, getting better Robust support
Type name handling / advanced metadata More manual Built-in conveniences
Community examples & plugins Growing Large ecosystem

Use System.Text.Json for most new projects where performance and minimal dependencies matter. Choose Newtonsoft.Json when you need advanced features (e.g., rich polymorphic scenarios, flexible contract resolvers, or older codebases already relying on it).


Basics: Serialization and Deserialization

Below are core patterns in both libraries.

  • Install: included in .NET; for additional features, reference System.Text.Json NuGet matching your runtime.
  • Common types: System.Text.Json.JsonSerializer, JsonSerializerOptions, JsonSerializerContext (source-gen).

Example: basic serialization/deserialization

using System.Text.Json; public class Person {     public string Name { get; set; }     public int Age { get; set; } } // Serialization var person = new Person { Name = "Alice", Age = 30 }; string json = JsonSerializer.Serialize(person); // Deserialization Person p2 = JsonSerializer.Deserialize<Person>(json); 

Customizing options:

var options = new JsonSerializerOptions {     WriteIndented = true,     PropertyNameCaseInsensitive = true,     PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; string json = JsonSerializer.Serialize(person, options); Person p = JsonSerializer.Deserialize<Person>(json, options); 

Performance tip: reuse JsonSerializerOptions instances; they are thread-safe after creation.

Source-generated serialization (for zero-allocation and high speed) — available via JsonSerializerContext and [JsonSerializable] attributes when appropriate for AOT or peak performance.

Newtonsoft.Json (Json.NET)

  • Install: NuGet package Newtonsoft.Json
  • Common types: JsonConvert, JsonSerializerSettings, JsonSerializer

Example:

using Newtonsoft.Json; public class Person {     public string Name { get; set; }     public int Age { get; set; } } string json = JsonConvert.SerializeObject(person, Formatting.Indented); Person p = JsonConvert.DeserializeObject<Person>(json); 

Customization example:

var settings = new JsonSerializerSettings {     Formatting = Formatting.Indented,     NullValueHandling = NullValueHandling.Ignore,     ContractResolver = new CamelCasePropertyNamesContractResolver() }; string json = JsonConvert.SerializeObject(person, settings); Person p = JsonConvert.DeserializeObject<Person>(json, settings); 

Common scenarios and how to handle them

1) Ignoring nulls or default values

  • System.Text.Json:
    
    options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; // or WhenWritingDefault 
  • Newtonsoft.Json:
    
    settings.NullValueHandling = NullValueHandling.Ignore; settings.DefaultValueHandling = DefaultValueHandling.Ignore; 

2) CamelCase property names

  • System.Text.Json: PropertyNamingPolicy = JsonNamingPolicy.CamelCase
  • Newtonsoft.Json: ContractResolver = new CamelCasePropertyNamesContractResolver()

3) Date/time formatting

  • System.Text.Json: use JsonSerializerOptions.Converters with JsonConverter or adjust DateTime handling via JsonSerializerOptions (but no built-in format string setting; custom converter often required).
  • Newtonsoft.Json: settings.DateFormatString = “yyyy-MM-ddTHH:mm:ssZ”

4) Polymorphic types

Polymorphism is an area where Newtonsoft.Json shines out-of-the-box with TypeNameHandling, but it can be dangerous (security implications for untrusted input). System.Text.Json requires custom converters or the experimental polymorphism features (and is improving).

Newtonsoft example:

var settings = new JsonSerializerSettings {     TypeNameHandling = TypeNameHandling.Auto }; string json = JsonConvert.SerializeObject(shape, settings); Shape s = JsonConvert.DeserializeObject<Shape>(json, settings); 

Caution: avoid enabling TypeNameHandling on untrusted JSON or use a SerializationBinder to restrict allowed types.

5) Streaming large JSON data

  • System.Text.Json provides Utf8JsonReader and Utf8JsonWriter for low-level, high-performance streaming parsing/writing.
  • Newtonsoft.Json provides JsonTextReader/JsonTextWriter and JsonSerializer for streaming scenarios.

Example of reading with Utf8JsonReader: use when you need to parse huge payloads without building object graphs in memory.

6) Handling missing or extra properties

  • By default, System.Text.Json throws for missing required properties only when [JsonRequired] is used; otherwise extra properties are ignored. Use PropertyNameCaseInsensitive to tolerate case differences.
  • Newtonsoft.Json will ignore extra properties by default; use MissingMemberHandling or attribute-driven settings to change behavior.

Advanced: Custom converters and attributes

Custom converters let you control exactly how types are (de)serialized.

System.Text.Json:

  • Implement JsonConverter and register with JsonSerializerOptions.Converters.Add(new YourConverter()).
  • For high performance and AOT scenarios, prefer source-generated converters via JsonSerializerContext.

Newtonsoft.Json:

  • Implement JsonConverter and override ReadJson/WriteJson, then add to JsonSerializerSettings.Converters.

Attributes:

  • System.Text.Json: [JsonPropertyName], [JsonIgnore], [JsonInclude], [JsonConverter]
  • Newtonsoft.Json: [JsonProperty], [JsonIgnore], [JsonConverter], [JsonObject]

Example (System.Text.Json custom converter skeleton):

using System; using System.Text.Json; using System.Text.Json.Serialization; public class DateOnlyConverter : JsonConverter<DateOnly> {     private const string Format = "yyyy-MM-dd";     public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)         => DateOnly.ParseExact(reader.GetString()!, Format);     public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)         => writer.WriteStringValue(value.ToString(Format)); } 

Security considerations

  • Never enable automatic type name handling on untrusted JSON without restricting allowed types. This can permit deserialization attacks.
  • Validate inputs and prefer safe deserialization patterns when handling untrusted content.
  • When using Newtonsoft.Json’s TypeNameHandling, set a custom SerializationBinder (ISerializationBinder) to whitelist types.
  • Avoid using BinaryFormatter or other insecure serializers when JSON alternatives exist.

Debugging tips

  • Pretty-print JSON (Formatting.Indented or WriteIndented = true) to inspect payloads.
  • Compare expected vs actual JSON with a JSON diff tool or simple tests.
  • Use JSON schema (or documentation) for APIs to validate structure; System.Text.Json has limited built-in validation—consider third-party schema validators when needed.
  • Log raw JSON payloads (with redaction of sensitive fields) when diagnosing issues.

Performance tips

  • Reuse JsonSerializerOptions / JsonSerializerSettings instances; they are relatively expensive to construct.
  • Prefer System.Text.Json for scenarios requiring high throughput and low allocations.
  • Use Span / Utf8JsonReader for parsing binary/UTF-8 payloads without intermediate strings.
  • For repeatable payload shapes, consider source-generated serializers in System.Text.Json to eliminate reflection costs.
  • Avoid unnecessary conversions (string -> bytes -> string). Work with streams when possible.

Interoperability and versioning

  • Add version fields or wrapping objects if your API may evolve. Design your DTOs to allow optional fields and unknown properties to be ignored safely.
  • Use DTOs (data transfer objects) instead of domain objects directly to decouple schema changes from business logic.
  • Consider nullable reference types and default values to make contract evolution clearer.

Quick checklist for starting a new .NET project using JSON

  1. Choose a library: System.Text.Json for performance and fewer dependencies; Newtonsoft.Json for advanced features.
  2. Define DTOs that represent the JSON contract — prefer simple, immutable structures where possible.
  3. Configure global JsonSerializerOptions/JsonSerializerSettings (naming policy, null handling, converters).
  4. Add custom converters for non-standard types (DateOnly, BigInteger, polymorphic hierarchies).
  5. Write tests to assert serialization round-trips and compatibility with external producers/consumers.
  6. Log and validate incoming JSON; sanitize sensitive fields in logs.
  7. Reuse options objects and prefer streaming APIs for large payloads.

Example: small end-to-end example using System.Text.Json

using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Threading.Tasks; public record Product(int Id, string Name, decimal Price); public class Program {     public static async Task Main()     {         var products = new List<Product>         {             new Product(1, "Laptop", 1299.99m),             new Product(2, "Mouse", 25.50m)         };         var options = new JsonSerializerOptions         {             WriteIndented = true,             PropertyNamingPolicy = JsonNamingPolicy.CamelCase         };         // Serialize to file (streamed)         await using var fs = File.Create("products.json");         await JsonSerializer.SerializeAsync(fs, products, options);         // Read back         await using var rs = File.OpenRead("products.json");         var loaded = await JsonSerializer.DeserializeAsync<List<Product>>(rs, options);         Console.WriteLine($"Loaded {loaded?.Count} products");     } } 

Final notes

  • For new projects prefer System.Text.Json for speed and integration; use Newtonsoft.Json when you need its richer feature set.
  • Design JSON contracts deliberately, test thoroughly, and use converters where necessary. With careful configuration and attention to security, JSON in .NET is powerful and efficient for both small and large applications.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *