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

Add AttributeTargets.Interface to JsonConverterAttribute #33112

Closed
Tracked by #45189
dlyz opened this issue Mar 3, 2020 · 10 comments · Fixed by #54922
Closed
Tracked by #45189

Add AttributeTargets.Interface to JsonConverterAttribute #33112

dlyz opened this issue Mar 3, 2020 · 10 comments · Fixed by #54922
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json
Milestone

Comments

@dlyz
Copy link

dlyz commented Mar 3, 2020

JsonConverterAttribute does not contain target AttributeTargets.Interface. I did not find any design notes about this behavior.

It's strange that JsonConverterAttribute can be applied to abstract classes, but not to interfaces.

In fact, JsonConverterAttribute can be used on property of an interface type, and it will work as expected. Also, specific JsonConverter that works with an interface type can be added to JsonSerializerOptions, and will work as expected.

Similar issue about enums: #30361

EDIT: See #33112 (comment) for the API proposal.

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Mar 3, 2020
@dlyz
Copy link
Author

dlyz commented Mar 4, 2020

I found a nice workaround. But still, I think it should be supported out of the box with JsonConverterAttribute.

[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
	public JsonInterfaceConverterAttribute(Type converterType)
		: base(converterType)
	{
	}
}
[JsonInterfaceConverter(typeof(MyConverter))]
public interface IMyInterface
{
	// ...
}

@steveharter
Copy link
Member

steveharter commented Mar 9, 2020

Are you assuming only supporting the first interface that has a [JsonConverter]?

Currently the converter model only allows for a single converter for a given Type (each property can have its own converter however).

@dlyz
Copy link
Author

dlyz commented Mar 10, 2020

I'm not sure I understand the question. The first interface of what? For example, I can write:

JsonSerializer.Deserialize<IMyInterface>(text);
JsonSerializer.Serialize<IMyInterface>(instance);

or

class MyClass
{
    public IMyInterface Instance { get; set; }
}

JsonSerializer.Deserialize<MyClass>(text);
JsonSerializer.Serialize<MyClass>(instance);

And if IMyInterface has a JsonConverterAttribute attribute, it doesn't matter whether IMyInterface is a class or an abstract class or an interface. So, there is no polymorphism involved on a serializer level, it may be involved only on a converter level, but it is up to user.

@steveharter
Copy link
Member

steveharter commented Mar 10, 2020

We could support interfaces today given the current semantics:

  1. There can only be one converter for a given type.
  2. Serialization is polymorphic when the type is System.Object (basically obj.GetType() is called to find the correct converter).
  3. We don't search base classes (or implemented interfaces) for converters.

This would be the behavior:

[JsonConverter(typeof(IFoo1Impl))] interface  IFoo1{ ... }
[JsonConverter(typeof(IFoo2Impl))] interface  IFoo2{ ... }
class MyClass : IFoo1, IFoo2 { ...  }

// No custom converter would get called
object obj = new MyClass();
JsonSerializer.Serialize<object>(obj);
interface IFoo1 : IFoo2, IFoo3 { ... }
[JsonConverter(typeof(IFoo2Impl))] interface  IFoo2 { ... }
[JsonConverter(typeof(IFoo3Impl))] interface  IFoo3 { ... }
class MyClass : IFoo1 { ...  }

// No custom converter would get called
IFoo1 obj = new MyClass();
JsonSerializer.Serialize<IFoo1>(obj);

Future polymorphism thoughts:
a) Support polymorphic serializaton on any type (not just System.Object)
b) Supporting polymorphic deserialization through "known types" (attributes and\or global registration)
c) Inspect base class for converters if none found on derived types (pending design discussion).

Potential future feature (c) would cause the examples above to be ambiguous with interfaces since there can be more than one "correct" answer. They are not ambiguous, however, when using inheritance since the most derived type \ converter would be selected.

@steveharter
Copy link
Member

Linking polymorphic features #29937 and #30083

@dlyz
Copy link
Author

dlyz commented Mar 10, 2020

b) Supporting polymorphic deserialization through "known types"

I don't understand what is the purpose of "known types" in your last example. Why does [KnownType(typeof(IFoo1))] should impact on the serialization of MyClass? It's more like feature (c). I thought "known types" are for base classes to enumerate all possible derived classes like this:

[KnownType(typeof(DerivedClass1))]
[KnownType(typeof(DerivedClass2))]
class BaseClass {}

class DerivedClass1 : BaseClass { }
class DerivedClass2 : BaseClass { }

[KnownType(typeof(DerivedClass1))]
[KnownType(typeof(DerivedClass2))]
class AnyClass
{
    public object Value { get; set; }
}

// can be DerivedClass1 or DerivedClass2 depending on type information in json
var obj = JsonSerializer.Deserialize<BaseClass>(json);
// can contain DerivedClass1 or DerivedClass2 depending on type information in json
var obj = JsonSerializer.Deserialize<AnyClass>(json);

In this case I don't understand how known types can be practically combined with custom converters at all.

c) Inspect base class for converters if none found on derived types (pending design discussion).

This behavior is kind of wierd for me. Also it's either a breaking change or a disabled-by-default option. But even if it will be an option (like UseBaseClassConverterIfExists?), it seems that it can lead to much less obvious behavior, than a few interfaces with converters. What if there is a runtime-provided (from options) converter for this base class?

And this feature is ok for serialization, but how will it behave for deserialization?


So, my opinion is if the type of a property or a root serialize-deserialize type has a converter attribute, it should be used, in other case runtime-provided or default converter should be used for that exact (declared) type (just like now, simple and working strategy). Known types is just a settings for the default converter which will support polymorphism through this known types.

Anyway, now converters for interfaces are not forbidden at all, they can be attached in options or with the workaround attribute (see above).

@steveharter
Copy link
Member

I don't understand what is the purpose of "known types" in your last example.

Yep thanks. My last example on known types was not correct. I just removed it since I don't want to presume the actual design for known types in this PR.

"Known types" is likely to be used for serialization to write a "safe" type name as metadata (one that doesn't include the full namespace and class type) which is used later during deserialization to map to the appropriate concrete Type to instantiate.

A potential issue I mentioned with interfaces is feature "c" where we inspect base classes and interfaces for a converter (if no converter found on the more derived type). If we decide to do that, however, it only makes sense to support base classes and not interfaces to avoid any ambiguity. By not implementing this feature for base classes means if we have a "abstract base class declared on a property" which has a converter but the "actual concrete derived class" (obtained via obj.GetType()) does not have a custom converter, then the custom converter on the base class will not be used and instead the default object converter will be used.

So in summary I don't see a reason to not allow converters for interfaces at this time.

@layomia
Copy link
Contributor

layomia commented Jul 10, 2020

From @jamalabo1 in #38939:

   public class MyClass
   {
       [JsonConverter(typeof(CustomBreakConverter))] 
       public IEnumerable<MyRange<DateTimeOffset>> Break { get; set; }
   }

Custom converter:

    public class CustomBreakConverter : JsonConverter<IEnumerable<MyRange<DateTimeOffset>>>
    {
        public override IEnumerable<MyRange<DateTimeOffset>> Read(ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options)
        {
            ...
        }

        public override void Write(Utf8JsonWriter writer, IEnumerable<MyRange<DateTimeOffset>> value,
            JsonSerializerOptions options)
        {
           ...
        }
    }

Controller

        [HttpGet]
        [Route("route")]
        public async Task<IActionResult> Get([FromQuery] MyClass data)
        {
           ...
        }

when the request is GET it ignores the custom converter, (throws an error (cannot deserialize) and does not reach the code (debugged))

        [HttpPost]
        [Route("route")]
        public async Task<IActionResult> Get([FromBody] MyClass data)
        {
           ...
        }

when the controller is this it works

@eiriktsarpalis
Copy link
Member

eiriktsarpalis commented Jun 5, 2021

Since custom converters for interfaces are already supported via property-level declarations or via adding them to JsonSerializationOptions.Converters, I don't see why we couldn't simply extend the attribute targets for JsonConverterAttribute:

namespace System.Text.Json.Serialization

-    [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Struct, AllowMultiple=false)]
+    [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Struct, AllowMultiple=false)]
    public class JsonConverterAttribute : JsonAttribute
    {
    }

Note that this should not have any bearing on how types implementing annotated interfaces are serialized, since as already alluded to in this conversation it would result in diamond ambiguity when resolving the serialization contract for a given value. This problem falls in the domain of polymorphic serialization and will be addressed independently via #29937 and #30083.

@eiriktsarpalis eiriktsarpalis modified the milestones: Future, 6.0.0 Jun 5, 2021
@eiriktsarpalis eiriktsarpalis added the api-ready-for-review API is ready for review, it is NOT ready for implementation label Jun 5, 2021
@eiriktsarpalis eiriktsarpalis self-assigned this Jun 5, 2021
@eiriktsarpalis eiriktsarpalis changed the title JsonConverterAttribute cannot be applied to an interface Add AttributeTargets.Interface to JsonConverterAttribute Jun 5, 2021
@eiriktsarpalis eiriktsarpalis removed the api-ready-for-review API is ready for review, it is NOT ready for implementation label Jun 5, 2021
@eiriktsarpalis eiriktsarpalis added the api-ready-for-review API is ready for review, it is NOT ready for implementation label Jun 7, 2021
@layomia layomia added the blocking Marks issues that we want to fast track in order to unblock other important work label Jun 28, 2021
@terrajobst
Copy link
Member

  • Looks good as proposed
namespace System.Text.Json.Serialization
{
    // Adding AttributeTargets.Interface
    [AttributeUsage(AttributeTargets.Class |
                    AttributeTargets.Struct |
                    AttributeTargets.Interface |
                    AttributeTargets.Enum |
                    AttributeTargets.Property |
                    AttributeTargets.Field, AllowMultiple = false)]
    public class JsonConverterAttribute : JsonAttribute
    {        
    }
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work labels Jun 29, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jun 29, 2021
eiriktsarpalis added a commit to eiriktsarpalis/runtime that referenced this issue Jul 12, 2021
eiriktsarpalis added a commit that referenced this issue Jul 13, 2021
* Allow JsonConverterAttribute usage on interfaces.

Fix #33112

* update ApiCompat baseline
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 13, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Aug 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Text.Json
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants