Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonConverter of the OneOf class for serialization #118

Open
ling921 opened this issue May 29, 2022 · 7 comments
Open

JsonConverter of the OneOf class for serialization #118

ling921 opened this issue May 29, 2022 · 7 comments

Comments

@ling921
Copy link

ling921 commented May 29, 2022

Example of OneOf<T0, T1>, here is the converter class

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

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 readonly Type _typeOfT0;
        private readonly Type _typeOfT1;
        private readonly JsonConverter<T0> _converterOfT0;
        private readonly JsonConverter<T1> _converterOfT1;

        public OneOfJsonConverter(JsonSerializerOptions options)
        {
            _typeOfT0 = typeof(T0);
            _typeOfT1 = typeof(T1);
            _converterOfT0 = (JsonConverter<T0>)options.GetConverter(_typeOfT0);
            _converterOfT1 = (JsonConverter<T1>)options.GetConverter(_typeOfT1);
        }

        public override OneOf<T0, T1> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            throw new JsonException("Cannot be deserialized.");
        }

        public override void Write(Utf8JsonWriter writer, OneOf<T0, T1> value, JsonSerializerOptions options)
        {
            if (value.IsT0)
            {
                if (_converterOfT0 is not null)
                    _converterOfT0.Write(writer, value.AsT0, options);
                else
                    JsonSerializer.Serialize(writer, value.AsT0, options);
            }
            else if (value.IsT1)
            {
                if (_converterOfT1 is not null)
                    _converterOfT1.Write(writer, value.AsT1, options);
                else
                    JsonSerializer.Serialize(writer, value.AsT1, options);
            }
            else
            {
                writer.WriteNullValue();
            }
        }
    }
}

Then use it on OneOf class

[JsonConverter(typeof(OnOfTwoValueConverter))]
public class OneOf<T0, T1>

In the WebApi project, we can use like this

public OneOf<T0, T1> Get()
{
    if (...)
        return T0;
    else
        return T1;
}
@romfir
Copy link
Contributor

romfir commented Jun 20, 2022

I think the converter could be simplified to only use properties from IOneOf interface

OneOf/OneOf/IOneOf.cs

Lines 3 to 7 in 014d68f

public interface IOneOf
{
object Value { get ; }
int Index { get; }
}

and it could be used for both serialization and deserialization

@zspitz
Copy link
Contributor

zspitz commented Aug 17, 2022

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 maps to string
  • null maps to the first nullable subtype
  • number maps to the first numeric subtype
  • True/False map to 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.

@ling921
Copy link
Author

ling921 commented Aug 17, 2022

I found a solution for deserialization, see dotnet/runtime#30083 (comment)

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();
        }
    }
}

@zspitz
Copy link
Contributor

zspitz commented Aug 17, 2022

@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.

@ling921
Copy link
Author

ling921 commented Aug 18, 2022

@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.

@agross
Copy link

agross commented Mar 5, 2023

@agross
Copy link

agross commented Mar 5, 2023

Here's a variant for System.Text.Json that (only!) supports OneOfBase-based DUs

using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

using OneOf;

namespace Infrastructure.Serialization;

public class OneOfConverterFactory : JsonConverterFactory
{
  public override bool CanConvert(Type typeToConvert)
    => typeof(IOneOf).IsAssignableFrom(typeToConvert);

  public override JsonConverter CreateConverter(Type? typeToConvert, JsonSerializerOptions options)
  {
    var (oneOfGenericType, converterType) = GetTypes(typeToConvert);
    if (oneOfGenericType is null || converterType is null)
    {
      throw new NotSupportedException($"Cannot convert {typeToConvert}");
    }

    var jsonConverter = (JsonConverter) Activator.CreateInstance(
      converterType.MakeGenericType(oneOfGenericType.GenericTypeArguments),
      BindingFlags.Instance | BindingFlags.Public,
      null,
      new object[] { options },
      null)!;

    return jsonConverter;
  }

  static (Type? oneOfGenericType, Type? converterType) GetTypes(Type? type)
  {
    while (type is not null)
    {
      if (type.IsGenericType)
      {
        var genericTypeDefinition = type.GetGenericTypeDefinition();
        if (genericTypeDefinition == typeof(OneOfBase<,>) ||
            genericTypeDefinition == typeof(OneOf<,>))
        {
          return (type, typeof(OneOf2JsonConverter<,>));
        }

        if (genericTypeDefinition == typeof(OneOfBase<,,>) ||
            genericTypeDefinition == typeof(OneOf<,,>))
        {
          return (type, typeof(OneOf3JsonConverter<,,>));
        }

        // TODO: Not supported (yet).
        // if (genericTypeDefinition == typeof(OneOfBase<,,,>) ||
        //     genericTypeDefinition == typeof(OneOf<,,,>))
        // {
        //   return (type, typeof(OneOfJson<,,,>));
        // }
        //
        // if (genericTypeDefinition == typeof(OneOfBase<,,,,>) ||
        //     genericTypeDefinition == typeof(OneOf<,,,,>))
        // {
        //   return (type, typeof(OneOfJson<,,,,>));
        // }
        //
        // if (genericTypeDefinition == typeof(OneOfBase<,,,,,>) ||
        //     genericTypeDefinition == typeof(OneOf<,,,,,>))
        // {
        //   return (type, typeof(OneOfJson<,,,,,>));
        // }
        //
        // if (genericTypeDefinition == typeof(OneOfBase<,,,,,,>) ||
        //     genericTypeDefinition == typeof(OneOf<,,,,,,>))
        // {
        //   return (type, typeof(OneOfJson<,,,,,,>));
        // }
        //
        // if (genericTypeDefinition == typeof(OneOfBase<,,,,,,,>) ||
        //     genericTypeDefinition == typeof(OneOf<,,,,,,,>))
        // {
        //   return (type, typeof(OneOfJson<,,,,,,,>));
        // }
        //
        // if (genericTypeDefinition == typeof(OneOfBase<,,,,,,,,>) ||
        //     genericTypeDefinition == typeof(OneOf<,,,,,,,,>))
        // {
        //   return (type, typeof(OneOfJson<,,,,,,,,>));
        // }
      }

      type = type.BaseType;
    }

    return (null, null);
  }

  static IOneOf CreateOneOf(JsonSerializerOptions options,
                            int index,
                            JsonDocument doc,
                            Type oneOfType,
                            Type[] types)
  {
    var args = new object[types.Length + 1];
    args[0] = index;
    args[index + 1] = doc.Deserialize(types[index], options);

    var oneOf = Activator.CreateInstance(
      oneOfType,
      BindingFlags.Instance | BindingFlags.NonPublic,
      null,
      args,
      null
    );

    return (IOneOf) oneOf;
  }

  const string IndexKey = "$index";

  class OneOf2JsonConverter<T0, T1> : JsonConverter<OneOfBase<T0, T1>>
  {
    static readonly Type OneOfType = typeof(OneOf<,>).MakeGenericType(typeof(T0), typeof(T1));
    static readonly Type[] Types = { typeof(T0), typeof(T1) };

    public OneOf2JsonConverter(JsonSerializerOptions _)
    {
    }

    public override OneOfBase<T0, T1> Read(ref Utf8JsonReader reader,
                                           Type typeToConvert,
                                           JsonSerializerOptions options)
    {
      using var doc = JsonDocument.ParseValue(ref reader);
      if (!doc.RootElement.TryGetProperty(IndexKey, out var indexElement) ||
          !indexElement.TryGetInt32(out var index) ||
          index is < 0 or > 1)
      {
        throw new JsonException("Cannot not find type index or type index is not a valid number");
      }

      var oneOf = CreateOneOf(options, index, doc, OneOfType, Types);

      return (OneOfBase<T0, T1>) Activator.CreateInstance(typeToConvert, oneOf);
    }

    public override void Write(Utf8JsonWriter writer,
                               OneOfBase<T0, T1> value,
                               JsonSerializerOptions options)
    {
      writer.WriteStartObject();

      writer.WritePropertyName(IndexKey);
      writer.WriteNumberValue(value.Index);

      using var doc = value.Match(
        t0 => JsonSerializer.SerializeToDocument(t0, typeof(T0), options),
        t1 => JsonSerializer.SerializeToDocument(t1, typeof(T1), options)
      );

      foreach (var prop in doc.RootElement.EnumerateObject())
      {
        prop.WriteTo(writer);
      }

      writer.WriteEndObject();
    }
  }

  class OneOf3JsonConverter<T0, T1, T2> : JsonConverter<OneOfBase<T0, T1, T2>>
  {
    static readonly Type OneOfType = typeof(OneOf<,,>).MakeGenericType(typeof(T0), typeof(T1), typeof(T2));
    static readonly Type[] Types = { typeof(T0), typeof(T1), typeof(T2) };

    public OneOf3JsonConverter(JsonSerializerOptions _)
    {
    }

    public override OneOfBase<T0, T1, T2> Read(ref Utf8JsonReader reader,
                                               Type typeToConvert,
                                               JsonSerializerOptions options)
    {
      using var doc = JsonDocument.ParseValue(ref reader);
      if (!doc.RootElement.TryGetProperty(IndexKey, out var indexElement) ||
          !indexElement.TryGetInt32(out var index) ||
          index is < 0 or > 2)
      {
        throw new JsonException("Cannot not find type index or type index is not a valid number");
      }

      var oneOfBase = CreateOneOf(options, index, doc, OneOfType, Types);

      return (OneOfBase<T0, T1, T2>) Activator.CreateInstance(typeToConvert, oneOfBase);
    }

    public override void Write(Utf8JsonWriter writer,
                               OneOfBase<T0, T1, T2> value,
                               JsonSerializerOptions options)
    {
      writer.WriteStartObject();

      writer.WritePropertyName(IndexKey);
      writer.WriteNumberValue(value.Index);

      using var doc = value.Match(
        t0 => JsonSerializer.SerializeToDocument(t0, typeof(T0), options),
        t1 => JsonSerializer.SerializeToDocument(t1, typeof(T1), options),
        t2 => JsonSerializer.SerializeToDocument(t2, typeof(T2), options)
      );

      foreach (var prop in doc.RootElement.EnumerateObject())
      {
        prop.WriteTo(writer);
      }

      writer.WriteEndObject();
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants