Open ling921 opened 2 years ago
I think the converter could be simplified to only use properties from IOneOf
interface
https://github.com/mcintyre321/OneOf/blob/014d68f723e1b772987f50c82016e6bfc09ee3f5/OneOf/IOneOf.cs#L3-L7
and it could be used for both serialization and deserialization
I've been recently trying to write a general System.Json.Text converter for OneOf
/OneOfBase
. It's out of scope for what I want to do, but I'm putting some thoughts down here.
OneOf
could have a converter factory, which would create the converter based on the generic parameters of the OneOf
; as described here.
A OneOfBase
-inheriting class might have additional properties. How would a converter be constructed which would handle those potentially added properties?
Write/serialization is trivial. For any OneOf
/OneOfBase
, the implementation would look like this, suitably extended:
public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) {
if (value.IsT0) {
JsonSerializer.Serialize(writer, value.AsT0, options);
} else { // value.IsT1 {
JsonSerializer.Serialize(writer, value.AsT1, options);
}
}
Read/deserialization is the real trouble. A OneOf
/OneOfBase
has multiple subtypes, generally unique. (If say string
is used as a subtype multiple times, how could a JSON string be resolved to one string
over the other? We could arbitrarily choose the first match.)
This means the converter would have to read the first token, and choose an appropriate subtype based on that token or on the entire value. Some possibilities are obvious:
string
boolean
But what happens for JsonTokenType.StartObject
? If you have 5 different subtypes each with an ID property, how do you resolve which object type to create, before wrapping it in OneOf
or OneOfBase
? Even worse, what do you do if multiple subtypes have the same property name/type?
And for JsonTokenType.StartArray
, you have to resolve the type of the array elements, and then figure out to which collection subtype to match it to.
I've written something similar for Newtonsoft.Json, which sort of worked for my needs.
I found a solution for deserialization, see https://github.com/dotnet/runtime/issues/30083#issuecomment-1002914110
This converter factory works fine, but is not as efficient as the native one
Here is the full code:
public class OnOfTwoValueConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(OneOf<,>);
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var _typeOfT0 = typeToConvert.GetGenericArguments()[0];
var _typeOfT1 = typeToConvert.GetGenericArguments()[1];
return (JsonConverter)Activator.CreateInstance(
typeof(OneOfJsonConverter<,>).MakeGenericType(new Type[] { _typeOfT0, _typeOfT1 }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { options },
culture: null)!;
}
private class OneOfJsonConverter<T0, T1> : JsonConverter<OneOf<T0, T1>>
{
private const string INDEX_KEY = "$index";
private readonly Type _typeOfT0;
private readonly Type _typeOfT1;
public OneOfJsonConverter(JsonSerializerOptions options)
{
_typeOfT0 = typeof(T0);
_typeOfT1 = typeof(T1);
}
public override OneOf<T0, T1> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
if (!doc.RootElement.TryGetProperty(INDEX_KEY, out var indexElement)
|| !indexElement.TryGetInt32(out var index)
|| index < 0
|| index > 1)
{
throw new JsonException("Cannot not find type index or type index is not a valid number.");
}
if (index == 0)
{
return doc.Deserialize<T0>(options);
}
else
{
return doc.Deserialize<T1>(options);
}
}
public override void Write(Utf8JsonWriter writer, OneOf<T0, T1> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WritePropertyName(INDEX_KEY);
writer.WriteNumberValue(value.Index);
using var doc = value.Match(
t0 => JsonSerializer.SerializeToDocument(t0, _typeOfT0, options),
t1 => JsonSerializer.SerializeToDocument(t1, _typeOfT1, options));
foreach (var prop in doc.RootElement.EnumerateObject())
{
prop.WriteTo(writer);
}
writer.WriteEndObject();
}
}
}
@ling921 There are a number of issues.
Firstly, the Write
override can be far simpler, as I noted above.
But more importantly, your converter works only for OneOf<T0, T1>
; it doesn't work for OneOf<T0, T1, T2>
or any OneOfBase<T0, T1>
-inheriting variant. Granted the only solution I see would be to have a separate converter factory for each OneOf
variant.
@zspitz Simply write a source generator like this https://github.com/mcintyre321/OneOf/blob/master/Generator/Program.cs
In the Write
method, you should write a metadata to specify the type of the data, and then the Read
method can read the type metadata and deserialize it to the specified type.
In the above example, I use $index
as the metadata key to store the index corresponding to the data.
Might be interesting to you how the F# people did it: https://github.com/Tarmil/FSharp.SystemTextJson/blob/master/docs/Customizing.md#unwrap-union-cases-with-a-record-field
Here's a variant for System.Text.Json that (only!) supports OneOfBase
-based DUs
Example of OneOf<T0, T1>, here is the converter class
Then use it on OneOf class
In the WebApi project, we can use like this