C# – How to use JsonExtensionData

Use the JsonExtensionData attribute (in System.Text.Json) to deserialize properties that aren’t part of the class. Without this, JSON fields that don’t match a property are simply ignored and the returned object will have null properties.

Here’s an example of adding this attribute to a dictionary property:

using System.Text.Json;
using System.Text.Json.Serialization;

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement> AdditionalProperties { get; set; }
}
Code language: C# (cs)

Now when you deserialize JSON to this Person class, any JSON field that’s not part of the class will be loaded into the dictionary. For example, the following JSON has three fields that aren’t part of the Person class:

{
  "FirstName": "Jason",
  "LastName": "Bourne",
  "Language": "C#",
  "IsRemote":true,
  "YearsExperience":10
}
Code language: JSON / JSON with Comments (json)

After deserializing, if you want to access the additional properties, you can get them from the dictionary, like this:

var person = JsonSerializer.Deserialize<Person>(json);

string lang = person.AdditionalProperties["Language"].GetString();
bool isRemote = person.AdditionalProperties["IsRemote"].GetBoolean();
int yearsExperience = person.AdditionalProperties["YearsExperience"].GetInt32();

Code language: C# (cs)

Use Dictionary<string, object> if you’re also going to serialize

Here’s how to pick which property type to use with JsonExtensionData:

  • Use Dictionary<string, JsonElement> if you only need to do deserialization.
  • Use Dictionary<string, object> if you need to do both deserialization and serialization.

Add objects to Dictionary<string, object> and serialize it

Using Dictionary<string, object> simplifies serialization (whereas JsonElement makes it harder). You can add any object to the dictionary and it’ll be serialized. Here’s an example:

using System.Text.Json;

var person = new Person()
{
    FirstName = "Jason",
    LastName = "Bourne",
    AdditionalProperties = new Dictionary<string, object>()
    {
        ["Language"] = "C#"
    }
};

Console.WriteLine(JsonSerializer.Serialize(person));
Code language: C# (cs)

This outputs the following JSON:

{"FirstName":"Jason","LastName":"Bourne","Language":"C#"}Code language: JSON / JSON with Comments (json)

Use deserialized Dictionary<string, object> values

Using Dictionary<string, object> makes deserialization harder. You have to cast the objects to JsonElement to get the underlying values, like this:

var person = JsonSerializer.Deserialize<Person>(json);

string lang = person.AdditionalProperties["Language"].ToString();//don't need to cast when it's a string
bool isRemote = ((JsonElement)person.AdditionalProperties["IsRemote"]).GetBoolean();
int yearsExperience = ((JsonElement)person.AdditionalProperties["YearsExperience"]).GetInt32();
Code language: C# (cs)

All the casting makes this quite verbose. If you don’t want to cast to JsonElement, you can use ToString() + a Convert.To method:

bool isRemote = Convert.ToBoolean(person.AdditionalProperties["IsRemote"]?.ToString());
Code language: C# (cs)

This approach works well if you also want to guard against nulls (because ?. will return a null, and the Convert.To methods return a default value if you pass in a null).

Errors to avoid

There are a few runtime exceptions you may run into when using JsonExtensionData. Even if you’re following these rules to avoid errors, be sure to test your code with realistic data to avoid surprises in production.

Only add JsonExtensionData to one property

If you try to add the JsonExtensionData attribute to multiple properties, you’ll get the following runtime exception:

System.InvalidOperationException: The type ‘Person’ cannot have more than one member that has the attribute ‘System.Text.Json.Serialization.JsonExtensionDataAttribute’

Remove JsonExtensionData from one of the properties.

This problem can also happen if you inherit from a class that’s already using JsonExtensionData. In that case, remove the attribute from your subclass.

Only use JsonExtensionData on a compatible property

If you add JsonExtensionData to an incompatible property, you get this exception:

System.InvalidOperationException: The data extension property ‘Person.AdditionalProperties’ is invalid. It must implement ‘IDictionary<string, JsonElement>’ or ‘IDictionary<string, object>’, or be ‘JsonObject’

You can only use it with types: Dictionary<string, object>, Dictionary<string, JsonElement>, and JsonObject.

Null check the JsonExtensionData property

When there’s no additional properties in the JSON, the JsonExtensionData property will be null. Do a null check before using it to avoid a runtime NullReferenceException.

var person = JsonSerializer.Deserialize<Person>("{}");

if (person.AdditionalProperties != null)
{
    //use the additional fields
}
Code language: C# (cs)

Null handling

If the client might send nulls in the additional properties, then you’ll need to guard against them. Nulls work differently depending on which property type you’re using with JsonExtensionData.

In the examples below, I’ll be deserializing the following JSON with a null property:

{
  "FirstName": "Jason",
  "LastName": "Bourne",
  "Language": null
}
Code language: JSON / JSON with Comments (json)

Nulls with Dictionary<string, object>

When you’re using JsonExtensionData with Dictionary<string, object>, a null in the JSON will be a null in the dictionary. Here’s an example:

var person = JsonSerializer.Deserialize<Person>(json);

object language = person.AdditionalProperties["Language"];
if (language is null)
{
    Console.WriteLine("Language property is null");
}
Code language: C# (cs)

The object is null, so this outputs:

Language property is nullCode language: plaintext (plaintext)

Nulls with Dictionary<string, JsonElement>

Null JSON values are deserialized as JsonElement with JsonValueKind.Null. So here’s how to null check:

var person = JsonSerializer.Deserialize<Person>(json);

var language = person.AdditionalProperties["Language"];
if (language.ValueKind != JsonValueKind.Null)
{
   //use the value since it's not null
}Code language: C# (cs)

JsonElement.GetString() handles nulls gracefully. It return a nullable string. Other primitive getters – such as JsonElement.GetBoolean() – throw an exception:

System.InvalidOperationException: The requested operation requires an element of type ‘Boolean’, but the target element has type ‘Null’.

Because of this inconsistency between different types, I’d suggest keeping it simple and check for JsonValueKind.Null.

TryGet methods don’t handle nulls

The JsonElement TryGet methods throw an exception if the value is null. So don’t use these TryGet methods to handle nulls gracefully.

Here’s an example. Let’s say I have an integer property that is null in the JSON:

{
  "FirstName": "Jason",
  "LastName": "Bourne",
  "YearsExperience": null
}
Code language: JSON / JSON with Comments (json)

Now deserialize this and use TryGetInt32():

var person = JsonSerializer.Deserialize<Person>(json);

person.AdditionalProperties["YearsExperience"].TryGetInt32(out int yearsExperience);
Code language: C# (cs)

This throws a runtime exception:

System.InvalidOperationException: The requested operation requires an element of type ‘Number’, but the target element has type ‘Null’

This is surprising behavior because normally you’d expect Try methods suppress exceptions and return false if there’s an error.

Leave a Comment