The Error event is an event handler found on JsonSerializer. The error event is raised whenever an exception is thrown while serializing or deserializing JSON. Like all settings found on JsonSerializer, it can also be set on JsonSerializerSettings and passed to the serialization methods on JsonConvert.
The JSON Formatter was created to help folks with debugging. As JSON data is often output without line breaks to save space, it can be extremely difficult to actually read and make sense of it. This tool hoped to solve the problem by formatting and beautifying the JSON data so that it is easy to read and debug by human beings. If you enable debugging into framework code (see this link) and then press ctrl + shift + e and select all managed code exceptions the error will appear in the actual source line that fails. You should be able to use the stack trace then to find out what part of the object it was trying to deserialize at that point.
In this example we are deserializing a JSON array to a collection of DateTimes. On the JsonSerializerSettings a handler has been assigned to the Error event which will log the error message and mark the error as handled.
The result of deserializing the JSON is three successfully deserialized dates and three error messages: one for the badly formatted string ('I am not a date and will error!'), one for the nested JSON array, and one for the null value since the list doesn't allow nullable DateTimes. The event handler has logged these messages and Json.NET has continued on deserializing the JSON because the errors were marked as handled.
One thing to note with error handling in Json.NET is that an unhandled error will bubble up and raise the event on each of its parents. For example an unhandled error when serializing a collection of objects will be raised twice, once against the object and then again on the collection. This will let you handle an error either where it occurred or on one of its parents.
If you aren't immediately handling an error and only want to perform an action against it once, then you can check to see whether the ErrorEventArgs's CurrentObject is equal to the OriginalObject. OriginalObject is the object that threw the error and CurrentObject is the object that the event is being raised against. They will only equal the first time the event is raised against the OriginalObject.
-->This article shows how to create custom converters for the JSON serialization classes that are provided in the System.Text.Json namespace. For an introduction to System.Text.Json
, see How to serialize and deserialize JSON in .NET.
A converter is a class that converts an object or a value to and from JSON. The System.Text.Json
namespace has built-in converters for most primitive types that map to JavaScript primitives. You can write custom converters:
- To override the default behavior of a built-in converter. For example, you might want
DateTime
values to be represented by mm/dd/yyyy format. By default, ISO 8601-1:2019 is supported, including the RFC 3339 profile. For more information, see DateTime and DateTimeOffset support in System.Text.Json. - To support a custom value type. For example, a
PhoneNumber
struct.
You can also write custom converters to customize or extend System.Text.Json
with functionality not included in the current release. The following scenarios are covered later in this article:
- Deserialize inferred types to object properties.
- Support polymorphic deserialization.
- Support round-trip for Stack<T>.
- Deserialize inferred types to object properties.
- Support Dictionary with non-string key.
- Support polymorphic deserialization.
- Support round-trip for Stack<T>.
In the code you write for a custom converter, be aware of the substantial performance penalty for using new JsonSerializerOptions instances. For more information, see Reuse JsonSerializerOptions instances.
Custom converter patterns
There are two patterns for creating a custom converter: the basic pattern and the factory pattern. The factory pattern is for converters that handle type Enum
or open generics. The basic pattern is for non-generic and closed generic types. For example, converters for the following types require the factory pattern:
Some examples of types that can be handled by the basic pattern include:
Dictionary<int, string>
WeekdaysEnum
List<DateTimeOffset>
The basic pattern creates a class that can handle one type. The factory pattern creates a class that determines, at run time, which specific type is required and dynamically creates the appropriate converter.
Sample basic converter
The following sample is a converter that overrides default serialization for an existing data type. The converter uses mm/dd/yyyy format for DateTimeOffset
properties.
Sample factory pattern converter
The following code shows a custom converter that works with Dictionary<Enum,TValue>
. The code follows the factory pattern because the first generic type parameter is Enum
and the second is open. The CanConvert
method returns true
only for a Dictionary
with two generic parameters, the first of which is an Enum
type. The inner converter gets an existing converter to handle whichever type is provided at run time for TValue
.
The preceding code is the same as what is shown in the Support Dictionary with non-string key later in this article.
Steps to follow the basic pattern
The following steps explain how to create a converter by following the basic pattern:
- Create a class that derives from JsonConverter<T> where
T
is the type to be serialized and deserialized. - Override the
Read
method to deserialize the incoming JSON and convert it to typeT
. Use the Utf8JsonReader that is passed to the method to read the JSON. You don't have to worry about handling partial data, as the serializer passes all the data for the current JSON scope. So it isn't necessary to call Skip or TrySkip or to validate that Read returnstrue
. - Override the
Write
method to serialize the incoming object of typeT
. Use the Utf8JsonWriter that is passed to the method to write the JSON. - Override the
CanConvert
method only if necessary. The default implementation returnstrue
when the type to convert is of typeT
. Therefore, converters that support only typeT
don't need to override this method. For an example of a converter that does need to override this method, see the polymorphic deserialization section later in this article.
You can refer to the built-in converters source code as reference implementations for writing custom converters.
Steps to follow the factory pattern
The following steps explain how to create a converter by following the factory pattern:
- Create a class that derives from JsonConverterFactory.
- Override the
CanConvert
method to return true when the type to convert is one that the converter can handle. For example, if the converter is forList<T>
it might only handleList<int>
,List<string>
, andList<DateTime>
. - Override the
CreateConverter
method to return an instance of a converter class that will handle the type-to-convert that is provided at run time. - Create the converter class that the
CreateConverter
method instantiates.
The factory pattern is required for open generics because the code to convert an object to and from a string isn't the same for all types. A converter for an open generic type (List<T>
, for example) has to create a converter for a closed generic type (List<DateTime>
, for example) behind the scenes. Code must be written to handle each closed-generic type that the converter can handle.
The Enum
type is similar to an open generic type: a converter for Enum
has to create a converter for a specific Enum
(WeekdaysEnum
, for example) behind the scenes.
Error handling
The serializer provides special handling for exception types JsonException and NotSupportedException.
JsonException
If you throw a JsonException
without a message, the serializer creates a message that includes the path to the part of the JSON that caused the error. For example, the statement throw new JsonException()
produces an error message like the following example:
If you do provide a message (for example, throw new JsonException('Error occurred')
, the serializer still sets the Path, LineNumber, and BytePositionInLine properties.
NotSupportedException
If you throw a NotSupportedException
, you always get the path information in the message. If you provide a message, the path information is appended to it. For example, the statement throw new NotSupportedException('Error occurred.')
produces an error message like the following example:
When to throw which exception type
When the JSON payload contains tokens that are not valid for the type being deserialized, throw a JsonException
.
When you want to disallow certain types, throw a NotSupportedException
. This exception is what the serializer automatically throws for types that are not supported. For example, System.Type
is not supported for security reasons, so an attempt to deserialize it results in a NotSupportedException
.
You can throw other exceptions as needed, but they don't automatically include JSON path information.
Register a custom converter
Register a custom converter to make the Serialize
and Deserialize
methods use it. Choose one of the following approaches:
- Add an instance of the converter class to the JsonSerializerOptions.Converters collection.
- Apply the [JsonConverter] attribute to the properties that require the custom converter.
- Apply the [JsonConverter] attribute to a class or a struct that represents a custom value type.
Registration sample - Converters collection
Here's an example that makes the DateTimeOffsetConverter the default for properties of type DateTimeOffset:
Suppose you serialize an instance of the following type:
Here's an example of JSON output that shows the custom converter was used:
C# Dynamic Json Deserialization
The following code uses the same approach to deserialize using the custom DateTimeOffset
converter:
Registration sample - [JsonConverter] on a property
The following code selects a custom converter for the Date
property:
The code to serialize WeatherForecastWithConverterAttribute
doesn't require the use of JsonSerializeOptions.Converters
:
The code to deserialize also doesn't require the use of Converters
:
Registration sample - [JsonConverter] on a type
Here's code that creates a struct and applies the [JsonConverter]
attribute to it:
Here's the custom converter for the preceding struct:
Debug Json Deserialization
The [JsonConvert]
attribute on the struct registers the custom converter as the default for properties of type Temperature
. The converter is automatically used on the TemperatureCelsius
property of the following type when you serialize or deserialize it:
Converter registration precedence
During serialization or deserialization, a converter is chosen for each JSON element in the following order, listed from highest priority to lowest:
[JsonConverter]
applied to a property.- A converter added to the
Converters
collection. [JsonConverter]
applied to a custom value type or POCO.
If multiple custom converters for a type are registered in the Converters
collection, the first converter that returns true for CanConvert
is used.
A built-in converter is chosen only if no applicable custom converter is registered.
Converter samples for common scenarios
The following sections provide converter samples that address some common scenarios that built-in functionality doesn't handle.
- Deserialize inferred types to object properties.
- Support polymorphic deserialization.
- Support round-trip for Stack<T>.
- Deserialize inferred types to object properties.
- Support Dictionary with non-string key.
- Support polymorphic deserialization.
- Support round-trip for Stack<T>.
Deserialize inferred types to object properties
When deserializing to a property of type object
, a JsonElement
object is created. The reason is that the deserializer doesn't know what CLR type to create, and it doesn't try to guess. For example, if a JSON property has 'true', the deserializer doesn't infer that the value is a Boolean
, and if an element has '01/01/2019', the deserializer doesn't infer that it's a DateTime
.
Type inference can be inaccurate. If the deserializer parses a JSON number that has no decimal point as a long
, that might result in out-of-range issues if the value was originally serialized as a ulong
or BigInteger
. Parsing a number that has a decimal point as a double
might lose precision if the number was originally serialized as a decimal
.
For scenarios that require type inference, the following code shows a custom converter for object
properties. The code converts:
true
andfalse
toBoolean
- Numbers without a decimal to
long
- Numbers with a decimal to
double
- Dates to
DateTime
- Strings to
string
- Everything else to
JsonElement
The following code registers the converter:
Here's an example type with object
properties:
The following example of JSON to deserialize contains values that will be deserialized as DateTime
, long
, and string
:
Without the custom converter, deserialization puts a JsonElement
in each property.
The unit tests folder in the System.Text.Json.Serialization
namespace has more examples of custom converters that handle deserialization to object
properties.
Debug Json Deserialization Python
Support Dictionary with non-string key
The built-in support for dictionary collections is for Dictionary<string, TValue>
. That is, the key must be a string. To support a dictionary with an integer or some other type as the key, a custom converter is required.
The following code shows a custom converter that works with Dictionary<Enum,TValue>
:
The following code registers the converter:
The converter can serialize and deserialize the TemperatureRanges
property of the following class that uses the following Enum
:
The JSON output from serialization looks like the following example:
Debug Json Deserialization Meaning
The unit tests folder in the System.Text.Json.Serialization
namespace has more examples of custom converters that handle non-string-key dictionaries.
Support polymorphic deserialization
Built-in features provide a limited range of polymorphic serialization but no support for deserialization at all. Deserialization requires a custom converter.
Suppose, for example, you have a Person
abstract base class, with Employee
and Customer
derived classes. Polymorphic deserialization means that at design time you can specify Person
as the deserialization target, and Customer
and Employee
objects in the JSON are correctly deserialized at run time. During deserialization, you have to find clues that identify the required type in the JSON. The kinds of clues available vary with each scenario. For example, a discriminator property might be available or you might have to rely on the presence or absence of a particular property. The current release of System.Text.Json
doesn't provide attributes to specify how to handle polymorphic deserialization scenarios, so custom converters are required.
The following code shows a base class, two derived classes, and a custom converter for them. The converter uses a discriminator property to do polymorphic deserialization. The type discriminator isn't in the class definitions but is created during serialization and is read during deserialization.
The following code registers the converter:
The converter can deserialize JSON that was created by using the same converter to serialize, for example:
The converter code in the preceding example reads and writes each property manually. An alternative is to call Deserialize
or Serialize
to do some of the work. For an example, see this StackOverflow post.
Support round trip for Stack<T>
If you deserialize a JSON string into a Stack<T> object and then serialize that object, the contents of the stack are in reverse order. This behavior applies to the following types and interface, and user-defined types that derive from them:
To support serialization and deserialization that retains the original order in the stack, a custom converter is required.
The following code shows a custom converter that enables round-tripping to and from Stack<T>
objects:
The following code registers the converter:
Handle null values
By default, the serializer handles null values as follows:
For reference types and Nullable<T> types:
- It does not pass
null
to custom converters on serialization. - It does not pass
JsonTokenType.Null
to custom converters on deserialization. - It returns a
null
instance on deserialization. - It writes
null
directly with the writer on serialization.
- It does not pass
For non-nullable value types:
- It passes
JsonTokenType.Null
to custom converters on deserialization. (If no custom converter is available, aJsonException
exception is thrown by the internal converter for the type.)
- It passes
This null-handling behavior is primarily to optimize performance by skipping an extra call to the converter. In addition, it avoids forcing converters for nullable types to check for null
at the start of every Read
and Write
method override.
To enable a custom converter to handle null
for a reference or value type, override JsonConverter<T>.HandleNull to return true
, as shown in the following example:
Other custom converter samples
The Migrate from Newtonsoft.Json to System.Text.Json article contains additional samples of custom converters.
The unit tests folder in the System.Text.Json.Serialization
source code includes other custom converter samples, such as:
If you need to make a converter that modifies the behavior of an existing built-in converter, you can get the source code of the existing converter to serve as a starting point for customization.
Comments are closed.