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.
System.Text.Json (recommended for new projects)
- 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
- Choose a library: System.Text.Json for performance and fewer dependencies; Newtonsoft.Json for advanced features.
- Define DTOs that represent the JSON contract — prefer simple, immutable structures where possible.
- Configure global JsonSerializerOptions/JsonSerializerSettings (naming policy, null handling, converters).
- Add custom converters for non-standard types (DateOnly, BigInteger, polymorphic hierarchies).
- Write tests to assert serialization round-trips and compatibility with external producers/consumers.
- Log and validate incoming JSON; sanitize sensitive fields in logs.
- 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.
Leave a Reply