runtime: Polymorphic Deserialization throws if $type metadata isn't present at the start of the object
Description
Attempting to deserialize a polymorphic structure with System.Text.Json that doesn’t feature $type
at start of the object results in an exception.
For my scenario, this currently prevents me from using the built-in polymorphism support with jsonb
columns in Postgres, as object properties have no guaranteed order.
Developer comment on this issue: https://github.com/dotnet/runtime/issues/63747#issuecomment-1158624112
Reproduction Steps
var databaseState = @"{
""BaseDictionary"": {
""6ed6e524-2ca4-4fea-8e21-7245ccb61863"": {
""Id"": ""6ed6e524-2ca4-4fea-8e21-7245ccb61863"",
""Name"": ""Something"",
""$type"": ""Derived"",
""OtherGuid"": ""d471c77d-5412-4e7a-a98d-8304e87792ed""
}
}
}";
JsonSerializer.Deserialize<WrapperType>(databaseState);
public record WrapperType(Dictionary<Guid, WrapperType.Base> BaseDictionary)
{
[JsonDerivedType(typeof(Derived), nameof(Derived))]
[JsonDerivedType(typeof(AlsoDerived), nameof(AlsoDerived))]
public abstract record Base(Guid Id, string Name);
public record Derived(Guid Id, string Name, Guid OtherGuid): Base(Id, Name);
public record AlsoDerived(Guid Id, string Name) : Base(Id, Name);
}
Expected behavior
Deserialization should work.
Actual behavior
System.NotSupportedException: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported.
Regression?
Limitation of initial implementation
Known Workarounds
I currently use PolyJson as an alternative, as the implementation reads ahead to find the discriminator.
Configuration
Impacts any version of STJ 7.0
Other information
No response
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 74
- Comments: 48 (12 by maintainers)
Commits related to this issue
- Remove polymorphism Remove polymorphism as it is not supported when the type discriminator is not the first property in the JSON document. See dotnet/runtime#72604. — committed to martincostello/alexa-london-travel by martincostello 7 months ago
- Remove polymorphism Remove polymorphism as it is not supported when the type discriminator is not the first property in the JSON document. See dotnet/runtime#72604. — committed to martincostello/alexa-london-travel by martincostello 7 months ago
- Remove polymorphism Remove polymorphism as it is not supported when the type discriminator is not the first property in the JSON document. See dotnet/runtime#72604. — committed to martincostello/alexa-london-travel by martincostello 7 months ago
- Remove polymorphism Remove polymorphism as it is not supported when the type discriminator is not the first property in the JSON document. See dotnet/runtime#72604. — committed to martincostello/alexa-london-travel by martincostello 7 months ago
Currently running into this issue. We are required to consume JSON from a predefined contract from an Open API-spec provided by a third party. They are using type discriminators liberally, and we cannot consume their requests because of this without making a workaround.
Type discrimination is part of Open API specs, but ordering of elements in JSON is by definition not a thing. I would prefer correctness by default on this issue, and rather have an optional spec-violating performance mode when you van control both the consumer and producer.
It’s currently not planned for inclusion in .NET 8
This is a huge issue. Azure Logic Apps adds the discriminator as the last property. So if we want to Deserialize a workflow that is already created using Azure Portal, we can’t deserialize just because of this. it’s a bummer.
We shouldn’t rely on the order of properties.
I can understand the performance impact, but my thinking is primary functionality isn’t working is a bigger issue than that.
This issue makes using the new
JsonDerivedType
attribute with EF Core impossible. Seems to me that two core .NET libraries should work together, right?What an unbelievably unreasonable requirement. You barely ever have any control over the ordering of properties in a JSON document, since most of the time it’s being received from some external API. This limitation makes no sense whatsoever, and is making this feature practically unusable in countless perfectly valid scenarios.
This honestly needs to be prioritized.
@eiriktsarpalis Any chance this makes it to .NET 8?
One thing that would help MASSIVELY is a better error message. I was aware of the current limitations but still spent the better part of an hour debugging a problem, until I realized that the fault lies in some old persisted JSON in which the (custom) type property wasn’t at the beginning of the JSON object.
Something like “type is polymorphic, but no type discriminator found” or “type discriminator not at beginning of the object” would have made the problem immediately clear, whereas the current message
is just mystifying, as the objects in question have an attributed constructor
I’m using Marten which under the hood uses Postgres its
jsonb
columns.jsonb
columns (re-)order JSON object keys by length:So a workaround for the case of Postgres is choosing a discriminator that’s shorter than any other object key (i.e. property name) like
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$t")]
There also seem to be an issue with the error messages.
If the Base type is abstract and the payload doesn’t have the typeDiscriminator as the first property then this error is shown:
However if the Base type is not abstract and the payload doesn’t have the typeDiscriminator as the first property then this error is shown:
It should say the latter in both cases.
I have this issue as well. When serializing JSON to MySQL using ef core, it seems that the ordering is not preserved so the data cannot be deserialized anymore.
There is a bit of a hack to work around this since MySQL seems to do deterministic ordering:
You might be able to get away with " " (space) as well, but basically a very low value ascii character that’s valid will cause it to be sorted first. Definitely would not recommend doing this in production, but I am using this for myself.
Perhaps it could peak to see if the type is on top and fall back to less performant behavior if not
In the meantime, if anyone’s interested, I’ve written a minimalistic
JsonPolymorphicConverter
that doesn’t have this limitation, which you can set up and use pretty easily if it fits your use case:Usage:
@aradalvand Agreed, this feature should either be deprecated with alternative recommendations put into the documentation, or should be or properly implemented. JSON spec doesn’t guarantee property order.
Per the spec:
Emphasis mine. Source: https://www.rfc-editor.org/rfc/rfc7159.html
I wrote a custom converter I’d like to share. I’m not sure how performant it is or if the error handling is up to snuff, but the code is straightforward. I think it works in .Net 6 too. Any feedback would be appreciated.
EDIT/UPDATE: I updated the code because serialization would only work if the type parameter was the base type (like
JsonSerializer.Serialize<BaseType>(subtypeInstance, options)
). New .Net fiddle to see it in action. Here’s the old fiddleSome features:
[JsonIgnore]
and[JsonPropertyName]
are respected[JsonPolymorphicAttribute]
/[JsonDerivedTypeAttribute]
isn’t necessaryBasically it’s:
JsonDocument
(JsonDocument.TryParseValue(...)
)doc.RootElement.GetProperty(discriminatorPropName)
)JsonObject
(JsonSerializer.SerializeToElement
,JsonObject.Create
)jObj[discriminatorPropName] = ...
)The code:
No, only the types decorated with a JsonDiscriminator, obviously. I would be fine with being able to disable fallback behavior on the attribute for cowboys who dont believe in specs
Speaking of solutions, I wrote a source generator for that: https://github.com/aviationexam/json-converter-source-generator/
which generates this and PolymorphicJsonConvertor
I was digging a bit and it seems that reference handling in custom converters is not possible. We do not have access to the
state
and therefore to the instance of theReferenceResolver
:I found this: https://github.com/dotnet/docs/issues/21777#issuecomment-736751404 but it’s a big hack and it will break when you use options multiple times.
@aradalvand This almost works for me, however my model contains nested polymorphic types (due to the RemoveThisFromOptions method which strips the type converter for any nested objects) Any recomendations to enable type converters for the nested polymorphic classes?
@eiriktsarpalis Do you have any estimate on when this issues might be resolved? Will it e.g. make it into net8?
@RobThree isn’t that
type
at the start of the object (representing an interface), as required by System.Text.Json? Your problem may be with recognizingtype
as opposed to$type
- in that case take a look at the docs.Yes, that’s probably what we would do once we get around to addressing the issue.
So it should fallback to suboptimal performance for, what, 90%, 95%, 99% of the JSON out there not containing any metadata fields?(only the types decorated with a JsonDiscriminator, obviously.)@eiriktsarpalis put it pretty well, already:
I think it becomes a question of whether we want System.Text.Json (STJ) to be spec compliant by default or performant by default.
Users handling JSON without metadata should IMHO get the best performance possible without having to enable a feature they don’t even know about. Users handling JSON containing metadata fields will run into these limitations and have to make a decision depending on their requirements.
E.g. when deciding between Postgres
json
andjsonb
columns and combining them with STJ, I can either have:jsonb
columns with better indexing and query performance but worse STJ deserialization performancejson
columns with worse indexing and query performance but better STJ deserialization performanceIt’s always a trade-off …
@dragorosson you might want to check out https://github.com/wivuu/Wivuu.JsonPolymorphism/
it does basically what you’re doing above but with a source generator
Why can’t I use my own property for the type discriminator?
My object has a
string Type
property already. According to this documentation it says:I would like to tell
System.Text.Json
that there is a property (the first one) namedType
, just use it.Correct, .NET 7 is currently in RC so feature development has been concluded.
No workaround that doesn’t involve writing a custom converter, unfortunately. In the future we might consider exposing a flag that reads ahead for type discriminators, but obviously that would come at the expense of performance so it would be turned off by default.
Is there any known workaround for this? I want to deserialize an object that does not come from
System.Text.Json
but has a discriminator that can be used that is of course not guaranteed to be first. The only thing I managed so far is to just deserialized first that one field and then specify type explicitly (i.e. not using the new polymorphic deserialization at all).Maybe there is something else that can be done, like e.g. explicit reordering or something like that?
Per https://github.com/dotnet/runtime/issues/63747#issuecomment-1158624112 this is a known limitation of the metadata reader. In the future we might consider adding read-ahead support specifically for type discriminators (I don’t believe reference preservation to have such requirements), but this would be at the expense of potentially buffering excessive data in the case of async serialization.