Skip to content

Support (serializing) sub types with generic type parameters when using JsonSubtypesConverterBuilder #135

Closed

Description

@EdeMeijer

Hello there!

Right now, JsonSubTypes does not (seem to) support serializing sub types that have generic type parameters, at least not in a generic way when using JsonSubtypesConverterBuilder to add a converter to the JsonSerializerSettings.

In my current project, I have a base type with many sub types and I register them using a converter builder. One of those sub types has generic type parameters, and it's not really an option to explicitly register all possible concrete types that might be used with it.

It would be cool if we could register a generic sub type without specifying type parameters, like .RegisterSubtype(typeof(Some<>), "Some"). At the very least, it would be nice if serializing objects of this type would work, and bonus points for also supporting deserialization (but this is probably hard as Json.NET would be responsible for this part).

Like said, I've considered registering the type with all possible concrete used types in the application, but this is brittle. I thought maybe I could scan the entire application for all uses of the generic type, but that's a lot of work.

I checked the code and saw that right now types for serialization are saved as dictionary keys. Naturally, the incoming type does not match since Some<int> is not equal to Some<T>. Maybe you could add a fallback in case serialization fails and the input has a generic type. Something like this (pseudo code):

if (!tryToSerializeWithType(input, inputType) && inputType.IsGenericType)
{
    if (!tryToSerializeWithType(input, inputType.GetGenericTypeDefinition()))
    {
        throw new Exception("Impossible to serialize type");
    }
}

Full example of what I'm trying to achieve:

using System;
using JsonSubTypes;
using Newtonsoft.Json;

namespace subTypesIssue
{
    class Program
    {
        static void Main()
        {
            var settings = new JsonSerializerSettings();
            settings.Converters.Add(JsonSubtypesConverterBuilder
                .Of<IResult>("Type")
                .RegisterSubtype(typeof(Some<>), "Some")
                .RegisterSubtype(typeof(None), "None")
                .SerializeDiscriminatorProperty()
                .Build());

            var input = new Some<int> {Value = 42};
            var json = JsonConvert.SerializeObject(input, settings);

            // Would expect {"Type":"Some","Value":42} but get "Impossible to serialize type" error
            Console.WriteLine(json);

            // Would be nice if deserialization also worked and the generic type could be inferred from the value
            var result = JsonConvert.DeserializeObject<IResult>(json, settings);

            Console.WriteLine(result);
        }
    }

    public interface IResult
    {
    }

    public class Some<T> : IResult
    {
        public T Value { get; set; }
    }

    public class None : IResult
    {
    }
}

Activity

  1. manuc66 commented on Sep 15, 2021

    @manuc66
    Owner

    Hi,

    Here is the not working solution https://dotnetfiddle.net/wBWRMw

    The is no enough information in .RegisterSubtype(typeof(Some<>), "Some") to know which target type need to be instantiated, how would you resole to the fact that it should deserialize to Some<int> ?

  2. EdeMeijer commented on Sep 17, 2021

    @EdeMeijer
    Author

    Yeah, I figured it would probably be impossible to deserialize. But what about only serialization? In this particular instance, that's really the only thing I need.

    That said, I'd understand if you would not want to add additional logic for a serialize-only solution.

  3. EdeMeijer commented on Sep 17, 2021

    @EdeMeijer
    Author

    Actually, this just made me think. Why not also use extra JSON properties to encode the generic type(s)? The above case could be serialized to {"Type": "Some", "T": "System.Int32", "Value": 42} by default, and you could introduce extra attributes to annotate the type parameters in order to customize the JSON property. Then this information could be used to determine the concrete type during deserialization.

    I can imagine that including raw system type names (and any actual type names for that matter) in JSON is not desirable (not portable, potential security risk). In the same fashion as the "known subtypes" approach, maybe the user could configure a custom mapping of supported generic types to type names to use in the serialized JSON. Something like this:

    [JsonSubtypes.KnownGenericType(typeof(int), "Integer")]
    [JsonSubtypes.KnownGenericType(typeof(string), "Text")]
    [JsonSubtypes.KnownGenericType(typeof(IResult), "Result")]
    public class Some<[JsonSubtypes.GenericType("ResultType")] T> : IResult
    {
        public T Value { get; set; }
    }

    With the above configuration, the serialized JSON would look like {"Type": "Some", "ResultType": "Integer", "Value": 42}.

    I understand this is quite the extension to your library and might be out of scope, but I think it could be a valuable addition for some users.

  4. manuc66 commented on Sep 29, 2021

    @manuc66
    Owner

    Hi @EdeMeijer

    The following sample does not work with the lines in comment, but it seems to be close to something that already exists:

    using System;
    using JsonSubTypes;
    using Newtonsoft.Json;
    
    namespace subTypesIssue
    {
        public class Program
        {
            public static void Main()
            {
          
                var input = new SomeInteger {Value = 42};
                var json = JsonConvert.SerializeObject(input);
    
                Console.WriteLine(json);
    			
                var result = JsonConvert.DeserializeObject<IResult>(json);
    
                Console.WriteLine(result);
            }
        }
    			
    	[JsonConverter(typeof(JsonSubtypes), "Type")]	
    	[JsonSubtypes.KnownSubType(typeof(Some<>), "Some")]												
    	public interface IResult
        {
    		string Type { get;}
        }
    
    	public class SomeInteger: Some<int> {
    		public override string ResultType { get { return "SomeInteger"; }} 
    		
    	}
    
    	public class SomeText: Some<string> {
    		public override string ResultType { get { return "SomeText"; }} 
    		
    	}
    
    	[JsonConverter(typeof(JsonSubtypes), "ResultType")]
    	//[JsonSubtypes.KnownSubType(typeof(SomeInteger), "SomeInteger")]
    	//[JsonSubtypes.KnownSubType(typeof(SomeText), "SomeText")]
    	public abstract class Some<T> : IResult
    	{
    	
    		public string Type { get { return "Some"; }} 
    		public abstract string ResultType { get; }
    		public T Value { get; set; }
    	}
    }

    See: https://dotnetfiddle.net/EGqf2C

    I've not debug yet the reason why...

  5. EdeMeijer commented on Sep 30, 2021

    @EdeMeijer
    Author

    That's... interesting. You're stacking two [JsonConverter] attributes in the same type hierarchy. But even is this would work, it would require the user to still declare every type he might be using as a sub type. Not exactly what I had in mind.

    That said, for my current use case, I worked around the problem in a different way (using come custom wrapper type) so I'd say, don't put too much effort into this if you don't feel like this is very important to you.

  6. manuc66 commented on Oct 22, 2021

    @manuc66
    Owner

    Some generic handling implemented in : #140

  7. added this to the 1.9.0 milestone on Oct 22, 2021
  8. manuc66 commented on May 9, 2022

    @manuc66
    Owner

    Published with version 1.9.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions