0

I'm working on an ActivityPub implimentation in c#, and sometimes links are "strings" like a url link, and sometimes links are objects with a Link sub-type. (Link : Entity)

I'm wondering if there's a possible way to use System.Text.Json to serialize a Link object as a string if a certain set of conditions are true (just write one string to the writer), and write the whole default object to the writer if the condition is not true.

I have tried following this solution: How to use default serialization in a custom System.Text.Json JsonConverter?, which still works on the code fiddle, but does not work for my implimentation and I'm not too sure why.

Does anyone know how I might debug this, or a better way to go about making sure Link : Entity objects can serialize into strings on occasion?

With that I get the following error:enter image description here

(in this case I have even tried to add the fetched default ctor to the modifiedOptions) Reguardless, it says that there's no data mapped for the Link class. I have also tried adding the JsonSerializeable attribute directly to the Link class.

The Error: Metadata for type 'ActivityPub.Types.Link' was not provided to the serializer. The serializer method used does not support reflection-based creation of serialization-related type metadata. If using source generation, ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.

My base code library: https://github.com/Meep-Tech/ActivityHub.Net/tree/collapse_links_during_serialization

The test code:

static void Main(string[] args) {
      Settings.DefaultContext = new Link("ActivityPub.Net.Testing");

      var testObject = new Object {
        Type = "Test",
        At = new Link("/terry") {
          Rels = new string[] {
            "test",
            "test2"
          }
        },
        Attribution = "/meep",
        Audience = new Link("/all") {
          Rel = "test"
        }
      };

      string json = testObject
        .Serialize();

      System.IO.File.WriteAllLines(
        "test.json",
        new[] { json }
      );

      Object @object = json.DeSerializeEntity<Object>();
      System.IO.File.WriteAllLines(
        "test1.json",
        new[] { @object.ToString() }
      );
    }
SuperMeip
  • 65
  • 3
  • 9
  • 1
    @SuperMeip post the code and exception text in the question itself. Images can't be copied, googled, compiled or tested. If you want to get help make it easy for people to help you. Don't force people to type your code or follow links – Panagiotis Kanavos Dec 07 '21 at 06:35
  • @PanagiotisKanavos the information you are talking about already seems to be there. What else can I include? Did you refresh the page? Also I think posting a link to a repository is easier than copy-pasting the entire repository in my post – SuperMeip Dec 07 '21 at 15:31
  • Any chance you could provide a [mcve] for your problem? – dbc Dec 10 '21 at 00:13
  • Microsoft recommends caching and reusing `JsonConverter` instances -- but that's purely for performance. What happens if you remove the `if (converter != null) converter.Write(writer, value, options);` logic and always do `JsonSerializer.Serialize(writer, value, options);`? By the way, I notice you seem to be mixing polymorphic serialization, compile-time serializer generation using the new `JsonSerializable` attribute, with custom converters, so a [mcve] would really help clarify where the problem. – dbc Dec 10 '21 at 01:02
  • See e.g. [STJ Source Generator fails to do polymorphic serialization for built-in types #58134](https://github.com/dotnet/runtime/issues/58134) for an example of compile time code generation in combination with polymorphism causing the *`Metadata for type ... was not provided to the serializer.`* If you can disentangle the various elements and narrow down the problem a little, you're more likely to get help here. See [ask]. – dbc Dec 10 '21 at 01:06
  • You really do need to create a [mcve] because your Github repo https://github.com/Meep-Tech/ActivityHub.Net/tree/collapse_links_during_serialization does not seem to have a license associated with it, and as such we can't copy it to reproduce the problem. See [ask]: *If it is possible to create a live example of the problem that you can link to (for example, on http://sqlfiddle.com/ or http://jsbin.com/) then do so - **but also copy the code into the question itself.** Not everyone can access external sites, and the links may break over time.* – dbc Dec 10 '21 at 17:14
  • Incidentally, in https://github.com/Meep-Tech/ActivityHub.Net/blob/collapse_links_during_serialization/Utilities/DefaultConverterFactory.cs, you copied my `DefaultConverterFactory` code from [this answer](https://stackoverflow.com/a/65430421/3744182) to [How to use default serialization in a custom System.Text.Json JsonConverter?](https://stackoverflow.com/q/65430420/3744182). Copying without crediting is a violation of the [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) license of that answer. – dbc Dec 10 '21 at 17:34
  • 1
    Incidentally, you seem to be using [`[JsonSerializable(typeof(Link))]`](https://github.com/Meep-Tech/ActivityHub.Net/blob/05d18532631b33ba765784078b2b95f91589ac1f/Types/Link.cs) wrongly. You are applying it to `Link` but, according to the [docs](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation?pivots=dotnet-6-0), it should be applied to some subclass of `JsonSerializerContext` for `Links`. – dbc Dec 13 '21 at 18:36

1 Answers1

2

In my original version of DefaultConverterFactory<T>, I cached the default converter because, in its documentation How to write custom converters for JSON serialization (marshalling) in .NET, Microsoft recommends, when serializing a complex object, to cache any required converters for performance reasons:

public DictionaryEnumConverterInner(JsonSerializerOptions options)
{
   // For performance, use the existing converter if available.
   _valueConverter = (JsonConverter<TValue>)options
       .GetConverter(typeof(TValue));

   // Cache the key and value types.
   _keyType = typeof(TKey);
   _valueType = typeof(TValue);
}

However, this has proven problematic for several reasons:

  1. When serializing a polymorphic value with a declared type of object, a non-functional converter is returned by GetConverter().

  2. When serializing numeric values, the converter returned ignores the NumberHandling setting.

  3. And now it appears you may have encountered a third problem: when using compile-time serializer source generation, the converter returned may not work.

That is enough problems to warrant ignoring Microsoft's recommendation. Simply DefaultConverter<T> as follows:

public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
    class DefaultConverter : JsonConverter<T>
    {
        readonly JsonSerializerOptions modifiedOptions;
        readonly DefaultConverterFactory<T> factory;

        public DefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory)
        {
            this.factory = factory;
            this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
        }

        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions);

        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => factory.Read(ref reader, typeToConvert, modifiedOptions);
    }

    protected virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions)
        => (T)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);

    protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions) 
        => JsonSerializer.Serialize(writer, value, modifiedOptions);

    public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert;

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new DefaultConverter(options, this);
}

public static class JsonSerializerExtensions
{
    public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
    {
        var copy = new JsonSerializerOptions(options);
        for (var i = copy.Converters.Count - 1; i >= 0; i--)
            if (copy.Converters[i].GetType() == converterType)
                copy.Converters.RemoveAt(i);
        return copy;
    }
}

Then, in any classes derived from DefaultConverterFactory<T> such as this one here, remove the final parameter JsonConverter<T> defaultConverter from Read() and Write(), and your code should now work.

(Incidentally, you seem to be using [JsonSerializable(typeof(Link))] wrongly. You are applying it to your model class Link but, according to the docs, it should be applied to some subclass of JsonSerializerContext for your model -- not the model itself.)

dbc
  • 104,963
  • 20
  • 228
  • 340