Debug Json Deserialization

broken image


If you have ever been given a JSON object and needed to parse it in Apex, then you may have run into some of the same issues I did so read on. JSON, which stands for JavaScript Object Notation, is widely used due to its ability to define the structure of an object and its values at the same time. For Apex purposes, it is used for many different purposes such as Web Service requests and responses as well as exchanging data with a Visualforce page so its pretty important to know how to handle it properly. While there is built-in Apex support, the documentation unfortunately leaves a little to be desired, leaving you in the dark. Read on if you want to learn more. To read more about the JSON specification, see here.

1. Understanding JSON Structure
2. Untyped Parsing of JSON Structure
3. Parsing JSON Structures in a Type-Safe Manner

1. Understanding JSON Structure

First it is important to understand at a very high level the structure of JSON.

JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON. Json.NET supports serialization callback methods. A callback can be used to manipulate an object before and after its serialization and deserialization by the JsonSerializer. Json.NET Documentation. Json.NET Documentation. Debugging with Serialization Tracing.

{} delimits the JSON object. Additionally, they are used within a JSON object to indicate key-value pairs.

[] denotes an array (or list) of records, each of which can be either a single value such as a Name or Sales Price, another array or a list of unlimited name-value pairs (such as properties about a Person – Name, Address, Phone, Email).

Serialize and deserialize JSON using C#, NET for some time, then you should know the excellent Json.NET serializer, also known as Newtonsoft.Json. So why do we need a new serializer Before.NET Core and even before NuGet, the most widely used library to serialize JSON is Newtonsoft.Jsonwith over 266 million downloads to date! Serializing and Deserializing JSON The quickest method of converting between JSON text and a.NET object is using the JsonSerializer. The JsonSerializer converts.NET objects into their JSON equivalent and back again by mapping the.NET object property names to the JSON property names and copies the values for you.

Let's use the following JSON object as an example throughout this article – this is a response from a web service that handles requests for Customer details. The JSON object contains 2 elements, one is an array of Customers and the other is a simple number, Count, containing the value of records processed successfully. For each Customer in the request, there is an array containing that Customer's properties.

In this example, there are three records in our JSON object, two of which were a success and one that contains an error.

To help identify the elements of a JSON object, blue boxes surround name-value pairs (properties) and red boxes surround the JSON object and its child objects.

Take note of the syntax.

  1. The entire JSON object is surrounded by curly braces {}.
  2. The JSON object has two properties: Customers and Count.
  3. The value of the Customers property is an array, which is denoted by square brackets [].
  4. The value of the Count property is a number.
  5. The array contains three child JSON objects.
  6. The first and second child JSON objects have a similar structure, each containing properties that have string values.
  7. Each property is represented as a name:value pair.
    1. Quotes around the property name are optional unless the name contains spaces in which case they are required.
    2. Quotes around the property value are required if it is a string value. For instance, Count's value is not in quotes as it is an Integer value.
  8. Properties are separated by commas. There is no comma after the last property in a JSON object.

So it is a Map (key-value pairs) with two keys:

  1. Customers
  2. Count

For the Customers key, there is an array of key-value pairs, one for each Customer requested, namely: John Smith, Jane Doe, and Michael Johnson.

So for above example, we have:

Key = Customers
Value = Array of 3 elements:

  1. {'Status':'Success','FirstName':'John','LastName':'Smith','Email':'jsmith@somewhere.com','Phone':'703.555.1212″}
  2. {'Status':'Success', 'FirstName':'Jane','LastName':'Doe','Email':'jdoe@somewhere.com','Phone':'540.555.1212″}
  3. {'Status':'Error','Message':'No Michael Johnson found.'}

For the Count key, there is a single value, the number of Customer records processed

Key = Count
Value = 2 // number of successful Customers found

2. Untyped Parsing of JSON Objects

Now that we understand the JSON structure, lets look at the untyped parsing provided by Salesforce. The top-level object corresponds to a Map in Apex. Any data contained within a [] maps to a List or Array of Objects.

For our example, our top-level object is a Map with two keys, the key is a String but the values are of different types (Array and Integer), so Object is used to achieve this. First, we represent the JSON object as a String for testing purposes.

Then the first key in our map is for Customers. Salesforce uses an Array or List of Objects to represent an array. So either one of these statements would work

Now for each array/list, there is a group of name-value pairs, which as we saw above, is represented by a Map in Salesforce as follows. This is a little awkward as our List is actually a List of Map objects.

And finally we get our count of successfully processed records.

This approach is error-prone and frankly just plain ugly. Thankfully, there is a better way to handle this map that is type-safe and much more readable.

3. Parsing JSON Objects in a Type-Safe Manner

Now that we have seen how to do it the hard way, lets do it the easy way! Once you know the structure of your JSON object, you can build Apex classes to hold the data in a object-oriented structural fashion and its quite simple. So for our JSON object example, we start with our top-level class: Customers.

A few tips for creating your classes.

  1. Data member names must exactly match (case-insensitive) the name of the attribute or element in the JSON object.
  2. All data members that are being mapped must have public access.
  3. All data members must be typed correctly to match the element type in the JSON object, otherwise a TypeException is thrown. So in our example, if Count is defined to be a String, an Exception is thrown as it should be defined as an Integer.
  4. NOTE: Interestingly enough, you do not have to define a default no-arg constructor. In fact, if you define a private no-arg Constructor , it still parses your object successfully.
  5. If you do not define a data member for one of the JSON object properties, then it will not be mapped. It will be silently ignored.

As we saw in Understanding JSON Structure, our top-level object has two keys, one for Customers (a List of Customer properties) and one for Count (a single Integer property). So we create a class with two variables named and typed appropriately.

public with sharing class CustomersResponse {
public List Customers;
public Integer Count;
public CustomersResponse() {
}

For each entry in the Customers List, we have a set of properties that may or may not be defined. In the success case, all the Customer details will be populated except Message. In the case of an error, only Status and Message are populated. So we add all possible fields to our Customer object as below.

public with sharing class Customer {
public String Status;
public String Message;
public String FirstName;
public String LastName;
public String Email;
public String Phone;
public Customer() {
}
}

Now that we have successfully defined our classes to represent our JSON structure, lets do the mapping. There are two ways to achieve this mapping with the standard Salesforce API. Both approaches are equally acceptable although the first approach is slightly cleaner as it involves one less API call.

Approach 1

Type resultType = Type.forName('CustomersResponse');
CustomersResponse deserializeResults = (CustomersResponse)JSON.deserialize(response, resultType);
System.debug('> deserialize() results = ' + deserializeResults);

NOTE: With the above code, in theory, it is possible to use JSON.deserialize(response,CustomersResponse.class), however this does not always work. Sometimes Salesforce is unable to determine the type correctly and you receive a 'variable does not exist : CustomerDetails.type' compile error message). So instead, you must first use a Type.forName() call to take care of this problem. Alternatively, you could compile all classes in your Salesforce Org together or define your class as an inner class of the class doing the deserialization work.

Approach 2

Type resultType = Type.forName('CustomersResponse');
CustomersResponse readValueAsResults = (CustomersResponse)JSON.createParser(response).readValueAs(resultType);
System.debug('> createParser().readValueAs() results = ' +readValueAsResults);

Tip: Testing JSON Objects

The best way to discover how to map your JSON object into a set of Apex classes is to take the web service (or mechanism that generates the JSON object) call entirely out of the equation. Create a String with your JSON object and then call JSON.deserializeUntyped() and see what gets return through the use of System.debug. This is easily done in a Unit Test or in an Execute Anonymous script.

Then once you have all the plumbing in place and want to perform end-to-end testing (crucial as web service calls are not invoked during unit tests), use your dear friend, the Execute Anonymous script, to call the method directly that invokes the web service. If your invocation of the web service is in a future method, place the logic inside a separate method with public access that is called directly from the Execute Anonymous script and have your future method call it directly.

Conclusion

At first glance, it may seem as if mapping a JSON object to a set of Apex classes is a lot of work and not worth it so you just use the untyped parser (I know that is what I thought when I first came across this integration). However, as I have hopefully demonstrated, that is not the case. Understanding the JSON structure and the nuances of the Salesforce JSON APIs, it is surprisingly straightforward and simple to map a JSON object to a set of Apex classes that you can navigate just like any other Apex class.

-->

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.
  • Deserialize inferred types to object properties.
  • Support Dictionary with non-string key.
  • Support polymorphic deserialization.
  • Support round-trip for Stack.

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
  • WeekdaysEnum
  • List

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. 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

Javascript

The following steps explain how to create a converter by following the basic pattern:

  • Create a class that derives from JsonConverter where T is the type to be serialized and deserialized.
  • Override the Read method to deserialize the incoming JSON and convert it to type T. 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 returns true.
  • Override the Write method to serialize the incoming object of type T. Use the Utf8JsonWriter that is passed to the method to write the JSON.
  • Override the CanConvert method only if necessary. The default implementation returns true when the type to convert is of type T. Therefore, converters that support only type T 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 for List it might only handle List, List, and List.
  • 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, for example) has to create a converter for a closed generic type (List, 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:

Debug Json Deserialization

The following steps explain how to create a converter by following the basic pattern:

  • Create a class that derives from JsonConverter where T is the type to be serialized and deserialized.
  • Override the Read method to deserialize the incoming JSON and convert it to type T. 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 returns true.
  • Override the Write method to serialize the incoming object of type T. Use the Utf8JsonWriter that is passed to the method to write the JSON.
  • Override the CanConvert method only if necessary. The default implementation returns true when the type to convert is of type T. Therefore, converters that support only type T 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 for List it might only handle List, List, and List.
  • 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, for example) has to create a converter for a closed generic type (List, 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:

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:

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.
  • Deserialize inferred types to object properties.
  • Support Dictionary with non-string key.
  • Support polymorphic deserialization.
  • Support round-trip for Stack.

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.

Debug Json Deserialization Tutorial

For scenarios that require type inference, the following code shows a custom converter for object properties. The code converts:

  • true and false to Boolean
  • 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.

Support Dictionary with non-string key

The built-in support for dictionary collections is for Dictionary. 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:

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:

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

If you deserialize a JSON string into a Stack 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 objects:

The following code registers the converter:

C# Dynamic Json Deserialization

Handle null values

By default, the serializer handles null values as follows:

  • For reference types and Nullable 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.
  • For non-nullable value types:

    • It passes JsonTokenType.Null to custom converters on deserialization. (If no custom converter is available, a JsonException exception is thrown by the internal converter for the type.)

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.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:

C# Json Deserialize

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.

Asp.net Core Debug Json Deserialization

Additional resources





broken image