-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Support polymorphic serialization through new option #29937
Comments
Moving to future for the following reasons:
|
I just ran into this scenario when doing some testing with latest preview. Is this something that is documented elsewhere? If not this should be highlighted in the migration guide as a new "feature" for new applications switching to use only the new JSON serializer. Otherwise someone might not notice the subtle change in their applications for a while. |
@steveharter The new custom converter feature cannot be used to handle polymorphic (de)serialization in some cases as, unless I'm missing something, because all properties have to be deserialized by hand / code. Which is not an option for maintainability when you have classes with more than two properties. See https://github.com/dotnet/corefx/issues/39031 The issue is that in the custom converter you only have a Utf8JsonReader which can read but not deserialize. Means you have to read one-by-one and set properties one-by-one. If there however would be a way to deserialize an object, but only the object not the whole json, within the custom converter all at once, it becomes a feature that can be used. |
@Symbai yes you are correct for deserialization -- there is not an easy way to support without manually authoring. Here's a test\sample for using a type discriminator to do that: Also here's a test\sample for polymorphic serialization. Currently the test\sample assumes the base class is abstract. I'll work on adding real doc and samples shortly. |
@steveharter Thanks but the samples cannot be used in real world scenarios because classes mostly don't have only 2 properties as shown in the sample, but ten to fifty. And now imagine we serialize something that itself has a class as property and this class also has another class and so on. So in the end, we have over hundreds of properties we, currently, have to write for EACH of them this here:
But if you have to write this more than 10 times already the code gets ugly and hardly maintainable. Example
|
This seriously needs to be documented somewhere, like on the System.Text.Json namespace, in highlighted text. I spent hours trying to track down why it was serializing empty strings while working in a Blazor project. |
This is bad.. just ran into this as well.. after hours of debugging :-( |
My own findings: And an article on how to "go back" to Newtonsoft.Json: https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/ |
On Newtonsoft it's super easy. For example just tell Newtonsoft all known Types and Newtonsoft does all the magic itself for you. No converter, no worries:
I really wish this would get more attention, at least set to 5.0 milestone instead of future. Currently it's just unusable for polymorphic situations and there is NO workaround (I consider the converter as no workaround in real world scenarios for the reason I posted earlier), so switching is not possible. |
Moving to 5.0. However this should also be tied to polymorphic deserialization support as well which is also a requested feature but much larger in scope. There are several requests for that and the implementation must be secure and opt-in through known types; we need to combine\triage these polymorphic issues -- here's one request: https://github.com/dotnet/corefx/issues/41347 |
Known types / magic polymorphism on the wire are bad patterns and I have seen people waste a ton of time on weird issues and bugs around this - the benefit aint worth it. Neither Json or XML are polymorphic by design and making them do it is a hack at best. That said we should document explicit type creation better and possibly have some patterns around this eg
|
@bklooste newtonsoft Json supports it, without weird issues and bugs around. Since the .NET Team is working together with the author of Newtonsoft Json, it's even more easier. And regarding XML, most of its code was written for more than a decade ago and remains untouched. Obviously it lacks of features and therefore is a bad comparison. |
As I needed only one-side serializer for specific base class (to make API return derived classes properties), I came up with current solution
|
@DenisSemionov, it seems like your sample escapes classes and writes data as string. To serialize normally json converter should be something like this (similar to @steveharter sample above):
... and then registered in JsonOptions:
|
@sfadeev , I agree with you, noticed my mistake too late. Can't find any good examples or documentation for polymorphic serialization in .net core 3.0, need to support 9 subclasses in one web API get method. I will share my solution if I come up with a smart one. |
@DenisSemionov, you can use sample from my comment above. |
@sfadeev, This works exactly as needed, much appreciate for provided example! |
I also ran into this issue, which seemed very counter-intuitive to me. I understand the issues with deserializing polymorphic types, but serialization should be trivial because the types are always known at runtime. Because of that, I somewhat disagree that this should be tied to deserialization support (which I expected to write custom code for anyway). Anyway, I have dealt with this by making a more generic version of the solution given by @sfadeev :
This allows the serializer to serialize any object derived from an abstract class:
Details property of RequestDto can now be serialized properly:
This has so far worked for me. |
We're going back to Newtonsoft for now. Here's a pretty unsophisticated workaround that might help someone else out.
|
I've also lost half a day on that debugging asp.net sources. As Newtonsoft's out-of-the-box behavior covers inheritence, i consider this as a real breaking change which will definitely lead to regression after migration to 3.0. I guess the 1st best is to make a red alert in corresponding section of the article Migrate from ASP.NET Core 2.2 to 3.0. |
+1 for a day lost to this. It's not just polymorphic class abstractions, it happens to interfaces too. If you make an interface I would go as far as saying making polymorphic serialization optional, and then not making it the default and then also not including it in 3.0 should be classified as a bug. |
I followed your example, and I got the exception:
As an example... One observation I've made is that only properties are handled by |
From @mauricio-bv in #31742
|
Then go ahead and move on. Stop commenting an OPT-IN feature request by only saying that you don't need it. Not only that you ping everyone who subscribes to it, which is really annoying, it's also completely redundant and helps no one. If you have the desire to tell the world about things you dislike, please use Twitter. If you have some constructive / technical feedback to share, you're welcome. FYI: This is not just you but also other people here, I just picked you because you are the last. Thanks in advance for yours and everyone else understanding and have a nice weekend. |
Any updates on whether this is being picked up in this lifetime? There are many issues like this that just seem to sit in limbo as updates to .NET Core and the surrounding ecosystem are pushed out. |
The answer is invariably "no", since Microsoft's figured out that hype-driven development is the next big thing. As in they announce and ship a cool new feature that works for 80% of scenarios, then pretend it's complete and thus never finish the outstanding 20%. I was hoping that with James Newton-King joining Microsoft, STJ would gain parity with Newtonsoft.Json, but it seems that James has been tasked to work on other new features that will inevitably also be delivered incomplete. |
I think tagged unions on dtos have been around for decades (used in Kernighan and Richie C ) and the performance difference between CreateType(type) and new T() is massive. switch ( dto.type) new mytype and setting some variables is a GOOD thing and the new c# features make this pretty, its simple code someone whose picked the language up for a few weeks can do it that cant go wrong vs signficant complexity burried in a lib which goes wrong in complicate ways.. KISS, remove the magic. I can write 10 simple converters in an hour and test them ( basically the time in this forum) .. it takes me days ( or weeks in some cases) to find and chase bugs or look at performance issues due to complex serialization paths hitting things like the reflection type cache limit. |
What I wanted to say with my comment is, that I don't want this feature if it means worse performance. At the moment STJ and Newtonsoft.Json complement eachother nicely. STJ has top performance and Newtonsoft.Json has all the bells and whistles. Pick whats more important to you. If STJ gets feature parity, then the performance will most likely suffer immensely and we have essentially another Newtonsoft.Json. So if you need all the features, why not just use Newtonsoft.Json and be happy with it. If the performance isn't a problem for you, then it's a great choice. I would like Microsoft to only implement new features for STJ, that don't sacrifice performance. We had some cases where Newtonsoft.Json took multiple seconds to serialize something and with STJ it is now almost instant. I don't want this problem back. Thanks. |
Proposed DesignConsider the following type hierarchy: public class Foo
{
public int A { get; set; }
}
public class Bar : Foo
{
public int B { get; set; }
}
public class Baz : Bar
{
public int C { get; set; }
} Currently, when serializing a Foo foo = new Foo { A = 1 };
Bar bar = new Bar { A = 1, B = 2 };
Baz baz = new Baz { A = 1, B = 2, C = 3 };
JsonSerializer.Serialize<Foo>(foo); // { "A" : 1 }
JsonSerializer.Serialize<Foo>(bar); // { "A" : 1 }
JsonSerializer.Serialize<Foo>(baz); // { "A" : 1 } Under the new proposal we can change this behaviour by annotating the base class (or interface) with the [JsonPolymorphicType]
public class Foo
{
public int A { get; set; }
} which will result in the above values now being serialized as follows: JsonSerializer.Serialize<Foo>(foo); // { "A" : 1 }
JsonSerializer.Serialize<Foo>(bar); // { "A" : 1, "B" : 2 }
JsonSerializer.Serialize<Foo>(baz); // { "A" : 1, "B" : 2, "C" : 3 } Polymorphism applies to nested values as well, for example: public class MyPoco
{
public Foo Value { get; set; }
}
JsonSerializer.Serialize(new MyPoco { Value = foo }); // { "Value" : { "A" : 1 } }
JsonSerializer.Serialize(new MyPoco { Value = bar }); // { "Value" : { "A" : 1, "B" : 2 } }
JsonSerializer.Serialize(new MyPoco { Value = baz }); // { "Value" : { "A" : 1, "B" : 2, "C" : 3 } } Note that the JsonSerializer.Serialize<Bar>(baz); // { "A" : 1, "B" : 2 } If annotating the base class with an attribute is not possible, polymorphism can alternatively be opted in for a type using the new public class JsonSerializerOptions
{
public Func<Type, bool> SupportedPolymorphicTypes { get; set; }
} Applied to the example above: var options = new JsonSerializerOptions { SupportedPolymorphicTypes = type => type == typeof(Foo) };
JsonSerializer.Serialize<Foo>(foo1, options); // { "A" : 1, "B" : 2 }
JsonSerializer.Serialize<Foo>(foo2, options); // { "A" : 1, "B" : 2, "C" : 3 } It is always possible to use this setting to enable polymorphism for every serialized type: var options = new JsonSerializerOptions { SupportedPolymorphicTypes = _ => true };
// `options` treats both `Foo` and `Bar` members as polymorphic
Baz baz = new Baz { A = 1, B = 2, C = 3 };
JsonSerializer.Serialize<Foo>(baz, options); // { "A" : 1, "B" : 2, "C" : 3 }
JsonSerializer.Serialize<Bar>(baz, options); // { "A" : 1, "B" : 2, "C" : 3 } As mentioned previously, this feature provides no provision for deserialization. If deserialization is a requirement, users would need to opt for the polymorphic serialization with type discriminators feature. Proposed APIsnamespace System.Text.Json
{
+ [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
+ public sealed partial class JsonPolymorphicTypeAttribute : JsonAttribute
+ {
+ public JsonPolymorphicTypeAttribute() { }
+ }
+ public class JsonSerializerOptions
+ {
+ public Func<Type, bool> SupportedPolymorphicTypes { get; set; }
+ }
} Open QuestionsNaming AmbiguityThis feature is orthogonal to #30083, which implements a different brand of polymorphism using type discriminators. Calling both features "polymorphism" is likely to confuse users who might expect the two API sets to be the same or compatible. They might reasonably expect that APIs serialized using this feature to be deserializable using APIs from #30083. We need to be careful about the names we pick so that they clearly illustrate that they are independent. Suggested names might include "Simple Polymorphism" or "Write-Only Polymorphism". JsonSerializerOptions.SupportedPolymorphicTypes uses a predicate instead of a classThe prototype uses a public interface IPolymorphicTypeResolver
{
bool CanBePolymorphic(Type type);
} |
@eiriktsarpalis thanks for the update. I generally like the proposed design. Just one thing: the name |
Thanks @thomaslevesque. I think you're onto something since I've already received the same feedback from someone else. For lack of a better alternative ( |
How about deserialzing and a tree of foo’s where nodes can be any subtype. For top level polymorphic behavior as in your foo example you can easily use ‘serialize < object >‘ to get similar behavior? besides I think you should be able to deserialize whatever you serialize. It would look really broken if my client serialize an object and the server can’t deserialize it. |
@qsdfplkj the particular feature has no provision for deserialization. This is a simple feature intended at extending the simple brand of polymorphism that is currently possible with Since it is not possible to roundtrip polymorphic values in nominal type systems without emitting some form of metadata on the wire, we are working on a separate feature that allows polymorphic deserialization using type discriminators (cf. #30083) |
@eiriktsarpalis So far it seems you can do all of it with |
This feature is orthogonal to #30083, which implements a different brand of polymorphism using type discriminators. Calling both features "polymorphism" is likely to confuse users who might expect the two API sets to be the same or compatible. They might reasonably expect that APIs serialized using this feature to be deserializable using APIs from #30083. We need to be careful about the names we pick so that they clearly illustrate that they are independent. Suggested names might include "Simple Polymorphism" or "Write-Only Polymorphism". |
I think its not needed because I can write such code already but SerializeRuntimeTypes vs SerializeDeclaredTypes is what this is about. |
API Review Feedback:
|
Here is a sketch of a model that might accommodate both designs: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class JsonPolymorphicAttribute : JsonAttribute
{
public JsonPolymorphicAttribute();
public JsonPolymorphicAttribute(string typeDiscriminatorPropertyName);
public string? TypeDiscriminatorPropertyName { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)]
public class JsonKnownTypeAttribute : JsonAttribute
{
public JsonKnownType(Type derivedType, string typeIdentifier);
public JsonKnownType(Type derivedType);
public Type DerivedType { get; }
public string? TypeIdentifier { get; }
} Here's how a hierarchy that doesn't emit any metadata can be defined: [JsonPolymorphic]
[JsonKnownType(typeof(Bat))]
public class Goo { }
public class Bat : Goo
{
public int A { get; set; }
}
JsonSerializer.Serialize<Goo>(new Bat { A = 42 }); // { "A" : 42 } And here's one that does: [JsonPolymorphic("_case")]
[JsonKnownType(typeof(Bat), "bat")]
public class Goo { }
public class Bat : Goo
{
public int A { get; set; } = 42;
}
JsonSerializer.Serialize<Goo>(new Bat { A = 42 }); // { "_case" : "bat", "A" : 42 } A public Dictionary<Type, (string? TypeDiscriminatorPropertyName, IEnumerable<(Type DerivedType, string? TypeIdentifier)>)> PolymorphicSerialization { get; }` |
Given the API review feedback I'm going to close this issue in favor of #30083, which will be revised to address both requirements. |
EDIT see #29937 (comment) for the finalized API proposal.
Original proposal by @steveharter (click to view)
Current behavior
Consider a simple model:
The current behavior, by design, is to use the static type and not serialize polymorphically.
However, if the type is declared to be
System.Object
then all properties will be serialized:In addition, the
Type
parameter can be specified:All of these semantics are current as will remain as-is.
Proposed API
Add an opt-in setting that will serialize all types polymorphically, instead of just
System.Object
:Example of new behavior:
This new behavior applies both to the root object being serialized (as shown above) plus any properties on POCOs including collections, such as
List<Person>
.Note: this issue does not enable any additional support for polymorphic deserialize. Currently, a property or collection of type
System.Object
will result in aJsonElement
being created. Any additional polymorphic deserialize support must be done with a custom converter.The text was updated successfully, but these errors were encountered: