-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Edited by @layomia.
Original post by @NN--- (click to view)
Description
JsonConverterAttribute is considered when used in a collection or as Dictionary value.
However, it is not considered when it is used as Dictionary key.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
public class OptionConverter : JsonConverter<Option>
{
public override Option? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, Option value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Value);
}
}
[JsonConverter(typeof(OptionConverter))]
public class Option
{
public string Value { get; set; }
}
public class P
{
static void Main()
{
var x = new Option {Value = "abc"};
Console.WriteLine(JsonSerializer.Serialize(x)); // OK
var y = new Option[] { new Option { Value = "abc" } };
Console.WriteLine(JsonSerializer.Serialize(y)); // OK
var z = new List<Option> { new Option { Value = "abc" } };
Console.WriteLine(JsonSerializer.Serialize(z)); // OK
var u = new Dictionary<string, Option> { { "abc", new Option { Value = "abc" } } };
Console.WriteLine(JsonSerializer.Serialize(u)); // OK
var d = new Dictionary<Option, string> {{new Option{Value = "abc"}, ""}};
Console.WriteLine(JsonSerializer.Serialize(d)); // TKey has unsupported type
}
}Please support JsonConvertAttribute on Dictionary keys.
Configuration
.NET 5.0.1
Regression?
No.
Other information
Today custom converters provided for types that appear in input graphs as dictionary keys (mostly primitives like string, numeric types, guids etc) cannot be used to handle dictionary keys. This manifests as NotSupportedException being thrown by the serializer when a custom converter is used for these primitive types & and the types are serialized as dictionary keys. We should provide API to override the NSE-throwing behavior and allow custom converters to handle dictionary keys.
In .NET 5, the (de)serialization of dictionary keys was handled entirely by the serializer even when custom converters were provided for the dictionary key types. This behavior was broken earlier in the .NET 6 timeline as a known side effect of internal STJ infrastructure changes. Now, custom converters are also invoked for dictionary keys, but the mechanism to support them is internal and defaults to throwing NotSupportedException for all converters that are not internal in STJ & are supported as dictionary keys.
API Proposal
This involves exposing the following internal virtual methods in the following form:
public abstract class JsonConverter<T> : JsonConverter
{
public virtual bool HandleNull => throw null;
public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
+ protected virtual T? ReadFromPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw null;
+ protected virtual void WriteToPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { }
}Notes
- We cannot have
nullvalues as dictionary keys, so theHandleNullproperty is not consulted when invoking theRead/WriteToPropertyNamemethods. For the same reason, wrt to nullability, we have a signature ofT ReadFromPropertyName(...)and notT? ReadFromPropertyName.... Similarly we[DisallowNull]for the inputTin the corresponding write method. - New methods are introduced instead of reusing the existing
ReadandWritemethods because the new functionality assumes that we are reading and writing strictly JSON property names, where the token type of the written value isJsonTokeType.PropertyName. To avoid user-confusion about whether to check the currentreader.TokenTypewhen reading, and whether to write values usingwriter.WritePropertyName, we introduce new methods where we can document the expected usage patterns. - The introduction of these APIs means that all serializable types can be serialized and deserialized as dictionary keys, provided that users provide implementations of the new methods. This includes complex types like POCOs and collections. This feature gives us functionality that was present in Newtonsoft.Json (which is based on
System.ComponentModel.TypeConverterinfrastructure). - The signatures of the new methods are based on the pre-existing
Read/Writemethods, e.g. passingJsonSerializerOptionsinstances & the type to convert on deserialization.
API usage
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Test
{
class Program
{
internal sealed class CustomStringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Assert(reader.TokenType == JsonTokenType.String);
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
=> writer.WriteStringValue(value);
public override string ReadFromPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Debug.Assert(reader.TokenType == JsonTokenType.PropertyName);
return reader.GetString();
}
protected override void WriteToPropertyName(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
=> writer.WritePropertyName(value);
}
static void Main()
{
JsonSerializerOptions options = new() { Converters = { new CustomStringConverter() } };
Dictionary<string, string> value = new() { ["key"] = "value" };
string json = JsonSerializer.Serialize(value, options);
Console.WriteLine(json); // {"key":"value"}
}
}
}