
Handling JSON Errors and Best Practices in .NET
Author - Abdul Rahman (Bhai)
JSON
5 Articles
Table of Contents
What we gonna do?
JSON serialization errors can crash your app at the worst possible moment. JsonException , circular references, invalid formats—they're all waiting to strike. In this article, we'll show you how to handle JSON errors gracefully and follow best practices that prevent these issues in the first place.
Why we gonna do?
Here's the truth about production code: exceptions happen. Malformed JSON from third-party APIs, unexpected null values, circular object references, or date formats that vary by region—any of these can bring down your application. The question isn't whether you'll encounter JSON errors, but how you'll handle them when they occur.
But there's good news: doing things right pays off. System.Text.Json provides robust error handling, flexible configuration options, and attributes that let you handle edge cases gracefully. From preserving circular references to deserializing into immutable types, the framework gives you the tools—you just need to know how to use them.
Think of it this way: unhandled exceptions are like driving without a seatbelt. Sure, most of the time you'll be fine. But when something goes wrong, proper error handling is what saves you .
How we gonna do?
Here's how to handle JSON errors and implement best practices in .NET:
Step 1: Understanding JsonException
System.Text.Json raises a JsonException when it encounters invalid JSON, exceeds maximum depth, or can't convert a value to the target property type. Always wrap deserialization in try-catch blocks when dealing with external data.
string jsonString = """
{
"productId": 123,
"expiryDate": "invalid-date-format"
}
""";
try
{
var product = JsonSerializer.Deserialize<Product>(jsonString);
}
catch (JsonException ex)
{
Console.WriteLine($"JSON Error: {ex.Message}");
// Log the error, return default values, or handle appropriately
}
/* Output:
JSON Error: The JSON value could not be converted to System.DateTime.
Path: $.expiryDate
*/
The exception message tells you exactly what went wrong and where—use this information for logging and debugging.
Step 2: Creating Custom JSON Exceptions
For domain-specific error handling, create custom exceptions that inherit from JsonException .
public class ProductJsonException : JsonException
{
public ProductJsonException() { }
public ProductJsonException(string message)
: base(message) { }
public ProductJsonException(string message, Exception innerException)
: base(message, innerException) { }
}
// Usage
try
{
var product = JsonSerializer.Deserialize<Product>(jsonString);
}
catch (JsonException ex)
{
// Log the original error
_logger.LogError(ex, "Failed to deserialize product JSON");
// Throw custom exception with context
throw new ProductJsonException(
"Product deserialization failed. Check date format.",
ex
);
}
Step 3: Allowing Invalid JSON (Comments and Trailing Commas)
Sometimes you receive JSON with comments or trailing commas—technically invalid, but still processable. Configure JsonSerializerOptions to handle these scenarios.
string jsonWithComments = """
{
// This is the temperature reading
"temperature": "25", // Quoted number
"summary": "Warm", // Trailing comma here
}
""";
var options = new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString |
JsonNumberHandling.WriteAsString
};
var weather = JsonSerializer.Deserialize<WeatherData>(
jsonWithComments,
options
);
Console.WriteLine($"Temperature: {weather.Temperature}");
// Output: Temperature: 25
Key options:
ReadCommentHandling.Skip- Ignores // and /* */ commentsAllowTrailingCommas = true- Permits commas after last array/object elementNumberHandling- Controls how numbers in quotes are handled
Step 4: Handling Overflow JSON with JsonExtensionData
What if the JSON contains extra properties not defined in your class? By default, they're ignored and lost. Use JsonExtensionData to capture them.
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
// Captures all extra properties
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
}
string json = """
{
"date": "2025-11-03",
"temperatureCelsius": 22,
"summary": "Warm",
"humidity": 65,
"pressure": 1013,
"windSpeed": 15
}
""";
var forecast = JsonSerializer.Deserialize<WeatherForecast>(json);
Console.WriteLine($"Main properties captured: {forecast.Summary}");
Console.WriteLine($"Extra properties: {forecast.ExtensionData.Count}");
foreach (var kvp in forecast.ExtensionData)
{
Console.WriteLine($" {kvp.Key}: {kvp.Value}");
}
/* Output:
Main properties captured: Warm
Extra properties: 3
humidity: 65
pressure: 1013
windSpeed: 15
*/
The best part? When you serialize the object back, ExtensionData properties are included as regular JSON properties—the object makes a perfect round trip.
string serializedBack = JsonSerializer.Serialize(
forecast,
new JsonSerializerOptions { WriteIndented = true }
);
Console.WriteLine(serializedBack);
/* Output: Original structure preserved!
{
"date": "2025-11-03T00:00:00",
"temperatureCelsius": 22,
"summary": "Warm",
"humidity": 65,
"pressure": 1013,
"windSpeed": 15
}
*/
Step 5: Handling Circular References
Circular references occur when an object references itself, directly or indirectly. Without proper handling, serialization throws an exception. You have two options: preserve references or ignore cycles .
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureCelsius { get; set; }
public List<WeatherForecast> HistoricalRecords { get; set; }
}
var todaysForecast = new WeatherForecast
{
Date = DateTime.Today,
TemperatureCelsius = -38, // Record low!
HistoricalRecords = new List<WeatherForecast>()
};
// Create a circular reference - today's forecast IS the record
todaysForecast.HistoricalRecords.Add(todaysForecast);
// Option 1: Preserve references with $id and $ref
var preserveOptions = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.Preserve,
WriteIndented = true
};
string jsonPreserve = JsonSerializer.Serialize(
todaysForecast,
preserveOptions
);
Console.WriteLine(jsonPreserve);
/* Output:
{
"$id": "1",
"date": "2025-11-03T00:00:00",
"temperatureCelsius": -38,
"historicalRecords": {
"$id": "2",
"$values": [
{
"$ref": "1"
}
]
}
}
*/
Notice the $id and $ref metadata—the serializer tracks objects and uses references
to avoid infinite loops.
// Option 2: Ignore cycles (sets circular references to null)
var ignoreOptions = new JsonSerializerOptions
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
};
string jsonIgnore = JsonSerializer.Serialize(todaysForecast, ignoreOptions);
Console.WriteLine(jsonIgnore);
/* Output:
{
"date": "2025-11-03T00:00:00",
"temperatureCelsius": -38,
"historicalRecords": [
null
]
}
*/
When to use which?
- Preserve: When you need to deserialize back and maintain object references
- IgnoreCycles: When you just need to serialize without errors and don't care about preserving circular references
Step 6: Deserializing to Immutable Types
What if your class has read-only properties or no setter? The serializer can't set values directly—use the JsonConstructor attribute.
public class WeatherForecast
{
public DateTime Date { get; }
[JsonPropertyName("celsius")]
public int TemperatureCelsius { get; }
public string Summary { get; } // No setter!
[JsonConstructor]
public WeatherForecast(
DateTime date,
int celsius,
string summary)
{
Date = date;
TemperatureCelsius = celsius;
Summary = summary;
}
}
string json = """
{
"date": "2025-11-03",
"celsius": -1,
"summary": "Freezing"
}
""";
var forecast = JsonSerializer.Deserialize<WeatherForecast>(json);
Console.WriteLine($"Summary: {forecast.Summary}");
// Output: Summary: Freezing
Important notes:
- Constructor parameter names must match property names (case-insensitive)
- Works even with
JsonPropertyNameattribute (use the property name, not the JSON name) - Only one constructor can have the
JsonConstructorattribute
Step 7: Handling Non-Public Property Accessors
Sometimes properties have private setters or getters. Use the JsonInclude attribute to allow serialization/deserialization.
public class WeatherForecast
{
public DateTime Date { get; set; }
[JsonInclude]
public int TemperatureCelsius { get; private set; } // Private setter
[JsonInclude]
public string Summary { private get; set; } // Private getter
}
string json = """
{
"date": "2025-11-03",
"temperatureCelsius": 40,
"summary": "Hot"
}
""";
var forecast = JsonSerializer.Deserialize<WeatherForecast>(json);
Console.WriteLine($"Temperature: {forecast.TemperatureCelsius}°C");
// Output: Temperature: 40°C (private setter worked!)
string serialized = JsonSerializer.Serialize(forecast);
Console.WriteLine(serialized);
// Output includes "summary" even with private getter
Step 8: Polymorphic Serialization
When a derived class has additional properties not in the base class, you must tell the serializer to use the derived type—otherwise, extra properties are lost.
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
}
public class SeismicForecast : WeatherForecast
{
public int TimeToNextEarthquake { get; set; }
public double Magnitude { get; set; }
}
var seismicForecast = new SeismicForecast
{
Date = DateTime.Today,
TemperatureCelsius = 22,
Summary = "Clear",
TimeToNextEarthquake = 48,
Magnitude = 3.5
};
// Wrong: Only serializes base class properties
string wrongJson = JsonSerializer.Serialize(seismicForecast);
// Only includes Date, TemperatureCelsius, Summary
// Correct: Specify the runtime type
string correctJson = JsonSerializer.Serialize(
seismicForecast,
seismicForecast.GetType(),
new JsonSerializerOptions { WriteIndented = true }
);
Console.WriteLine(correctJson);
/* Output: All 5 properties included!
{
"date": "2025-11-03T00:00:00",
"temperatureCelsius": 22,
"summary": "Clear",
"timeToNextEarthquake": 48,
"magnitude": 3.5
}
*/
Alternatively, declare the variable as object to force the serializer to use the runtime type:
object forecast = new SeismicForecast { /* ... */ };
string json = JsonSerializer.Serialize(forecast);
// Automatically uses derived type
Summary
JSON errors are inevitable, but with proper error handling and configuration , they don't have to crash your application. Wrap deserialization in try-catch blocks, configure options to handle invalid JSON, use JsonExtensionData to preserve overflow properties, and choose the right strategy for circular references. For immutable types and non-public accessors, leverage JsonConstructor and JsonInclude attributes. Follow these best practices, and your JSON serialization will be robust, flexible, and production-ready.