System.Text.Json – Deserialize properties that aren’t part of the class

Use the JsonExtensionData attribute to simplify accepting additional properties in JSON that aren’t part of the class you’re deserializing to.

To use this attribute, add a compatible* property to the class and apply the JsonExtensionData attribute:

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)

*Compatible property types you can use are Dictionary<string, object>, Dictionary<string, JsonElement> and JsonObject.

Any property in the JSON that’s not part of the class will get deserialized into this dictionary property. For example, let’s say the client sends the following JSON with three additional properties 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 this, you can access the additional properties through the dictionary. These are JsonElement objects, so you can convert them to the underlying types with GetString() / GetBoolean() / GetInt32() etc…:

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

By default, it’ll deserialize additional properties into JsonElement objects, even if you’re using Dictionary<string, object>. This means you have to cast the objects to JsonElement to really be able to use them. Therefore:

  • 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

When you’re building an object to serialize, using Dictionary<string, object> makes it easy to add any object (whereas JsonElement makes it hard). 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

When you’re using Dictionary<string, object> due to needing it for serialization, this will make deserialization harder, because you’ll have to cast the objects in the dictionary to JsonElement to get the underlying values. Here’s an example:

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’

It’s simple enough to avoid this exception if you’re only dealing with one class that you control.

It’s much more likely to happen unexpectedly if you’re inheriting from a class that’s already using the JsonExtensionData attribute (especially if you don’t control the class you’re inheriting from). In that case, you’ll have to remove the attribute in your subclass to avoid the exception.

Only use JsonExtensionData on a compatible property

You can only apply the JsonExtensionData attribute on properties of type Dictionary<string, object>, Dictionary<string, JsonElement>, or JsonObject. If you apply it to an incompatible type, then you’ll get the following runtime exception:

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

The problem is straightforward. You have to use one of the compatible types.

As an example, let’s say you know all of the additional properties are going to be bools, because they’re coming from checkbox fields on the client-side. You might reasonably think that you can use the following:

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

But this’ll result in a runtime InvalidOperationException. Instead, in this scenario use Dictionary<string, JsonElement> and then convert the JsonElement objects to booleans when you need the values:

var person = JsonSerializer.Deserialize<Person>(json); bool isRemote = person.AdditionalCheckboxes["IsRemote"].GetBoolean(); if (isRemote) { Console.WriteLine("The dev works remotely"); }
Code language: C# (cs)

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)

Check for the property key

If a property doesn’t exist in the JSON, it won’t be put in the dictionary. And if you try to access a non-existent key on the dictionary, you’ll get a KeyNotFoundException. To guard against this, check if it exists before using it.

int? yearsExperience; if (person.AdditionalProperties.TryGetValue("YearsExperience", out JsonElement jsonElement)) { yearsExperience = jsonElement.GetInt32(); Console.WriteLine(yearsExperience); }
Code language: C# (cs)

On the other hand, if you expect an additional property to always exist under certain conditions, then you can keep it simple and let the runtime KeyNotFoundException happen (otherwise you have to throw your own exception).

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 null
Code language: plaintext (plaintext)

Nulls with Dictionary<string, JsonElement>

When you’re using JsonExtensionData with Dictionary<string, JsonElement>, a null in the JSON will be deserialized to a JsonElement object with JsonValueKind.Null.

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 when you use the Try Pattern, the Try methods are supposed to return false if they fail – instead of throwing an exception. In my opinion, this is a design flaw in these TryGet methods.

Leave a Comment