-1

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", "test@test.com");
        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();
}
Vic F
  • 1,143
  • 1
  • 11
  • 26
  • There are several ways to ensure that the `object` values inside the dictionary are .NET primitive types rather than `JsonElement` values, but you will not be able to ensure that the `"dob"` round-trips as `DateOnly`. That's because JSON [has no date or datetime primiitive](https://stackoverflow.com/a/15952652). So the value of `"dob"` would come back as a string. Would that be sufficient? If not, you will need to modify your JSON information to convey type information. – dbc Jun 22 '23 at 20:57
  • @dbc "There are several ways to ensure that the object values inside the dictionary are .NET primitive types rather than JsonElement values" . . . would you mind expanding on that thought? – Vic F Jun 22 '23 at 21:02
  • Also, I have converters for DateTime and DateOnly, but i haven't plugged them in yet; do you think that's a variable in this scenario? – Vic F Jun 22 '23 at 21:08
  • If you have an `object` value, you can deserialize it as a primitive using `ObjectAsPrimitiveConverter` from [this answer](https://stackoverflow.com/a/65974452/3744182) to [C# - Deserializing nested json to nested Dictionary](https://stackoverflow.com/q/65972825/3744182). That being said, I'm not sure converters are invoked for `[JsonExtensionData]` so you might need to do some postprocessing in an `IJsonOnDeserialized.OnDeserialized()` callback as in [this answer](https://stackoverflow.com/a/75291082/3744182). – dbc Jun 22 '23 at 21:09
  • *Also, I have converters for DateTime and DateOnly* -- the problem is that the C# value is declared as `object` and the JSON token is a string, so there's no way the converter will get invoked. You would need to consider using a different JSON format where, instead of primitive values, you serialize an object with the type and the value, such as the `TypeWrapper` from [this answer](https://stackoverflow.com/a/61900664/3744182) to [NewtonSoft SerializeObject ignores TypeNameHandling](https://stackoverflow.com/q/61794579/3744182). – dbc Jun 22 '23 at 21:11
  • Now if you want to use some heuristics that recognize `DateOnly`, `TimeOnly` and `DateTime` strings, and convert them to the appropriate type, you could do that in a converter, or an `IJsonOnDeserialized.OnDeserialized()` callback. But if you do, a string that coincidentally matches the format will not round trip. – dbc Jun 22 '23 at 21:15

1 Answers1

-2

For the record, 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);
    }
}
Vic F
  • 1,143
  • 1
  • 11
  • 26