Search code examples
c#system.text.json

System.Text.Json Deserialize dictionary with JsonExtensionData attribute


I'm serializing/deserializing a dictionary<string,object>, but when I deserialize, instead of the values being objects, they are JsonElements.

I have a unit test that demonstrates the problem. How can I deserialize these values to objects? Can anyone help? Do I have to write a custom converter?

    [Test]
    public void SerializationTest()
    {
        var coll = new PreferenceCollection();
        coll.Set("email", "[email protected]");
        coll.Set("age", 32);
        coll.Set("dob", new DateOnly(1991, 2, 14));

        var json = JsonSerializer.Serialize(coll);
        Assert.NotNull(json);

        var clone = JsonSerializer.Deserialize<PreferenceCollection>(json);
        Assert.NotNull(clone);
        Assert.IsNotEmpty(clone!.Values);

        foreach (var kvp in clone.Values)
        {
            Assert.That(kvp.Value is not null);

            // Test fails here because kvp.Value is JsonElement.
            Assert.That(kvp.Value!.Equals(coll.Values[kvp.Key]));
        }
    }
using System.Text.Json.Serialization;

namespace MyCompany.Preferences;

public class PreferenceCollection
{
    public PreferenceCollection()
    {
        Values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
    }

    [JsonExtensionData] public IDictionary<string, object> Values { get; set; }

    public object Get(string key)
    {
        if (Values.TryGetValue(key, out object value))
        {
            return value;
        }

        return null;
    }

    public T Get<T>(string key)
    {
        if (Values.TryGetValue(key, out var value))
        {
            return (T)value;
        }

        return default;
    }

    public void Set(string key, object value)
    {
        if (value == null)
        {
            Remove(key);
        }
        else
        {
            if (!Values.TryAdd(key, value))
            {
                Values[key] = value;
            }
        }
    }

    public void Remove(string key) => Values.Remove(key);

    public void Clear() => Values.Clear();
}

Solution

  • This was solved with a JSON converter for object.

    This was my starting point: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-7-0#deserialize-inferred-types-to-object-properties

    /// <summary>
    /// <seealso href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-7-0#deserialize-inferred-types-to-object-properties"/>
    /// </summary>
    public sealed class ObjectJsonConverter : JsonConverter<object>
    {
        public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.String)
            {
                string str = reader.GetString()!;
    
                if (Guid.TryParse(str, out Guid g))
                {
                    return g;
                }
    
                if (DateOnly.TryParse(str, out DateOnly date))
                {
                    return date;
                }
    
                if (DateTime.TryParse(str, out _))
                {
                    return reader.GetDateTime();
                }
    
                return str;
            }
    
            if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt64(out long l))
            {
                if (l is >= int.MinValue and <= int.MaxValue && reader.TryGetInt32(out int i)) return i;
                return l;
            }
    
            return reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number => reader.GetDouble(),
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };
        }
    
        public override void Write(Utf8JsonWriter writer, object? objectToWrite, JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, objectToWrite, objectToWrite?.GetType() ?? typeof(object), options);
        }
    }
    

    The converter can then be used like this:

    var jsonOptions = new JsonOptions();
    jsonOptions.Converters.Add(new ObjectJsonConverter());
    
    var myDictionary = JsonSerializer.Deserialize<string, object>(someJson, jsonOptions);