Description
Description
While continuing the migration of the Microsoft Store to System.Text.Json (we're getting there! 😄), I'm now migrating some unstructured JSON data we have (which currently deserializes to a dictionary and then uses a whole bunch of manual lookups, which is pretty bad), to instead using proper strongly typed models with System.Text.Json. Due to how some of these models are crafted, we end up needing a few custom converters to make the whole thing work correctly. This is fine, but I've noticed some weird interplay between custom converters and the JSON source generators that I'm not sure is by design, so I figured I'd ask 🙂
Reproduction Steps
Consider this:
[JsonSerializable(typeof(A))]
public partial class MyContext : JsonSerializerContext
{
}
[JsonConverter(typeof(AConverter))]
public class A
{
public B B { get; set; }
}
public class B
{
}
public class AConverter : JsonConverter<A>
{
public override A? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, A value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
In this case, AConverter
will deserialize A
instances, with some custom logic. As part of this, it will also create a B
instance by deserializing the corresponding property in the JSON. That is, after moving the reader to the start of the object token, it'll just call JsonSerializer.DeserializeObject<B>(ref reader, options)
. We use this pattern extensively in our JSON contracts.
Expected behavior
I wouldn't expend the annotation to cause the source generator to stop traversing the type hierarchy.
Actual behavior
If you annotate A
with [JsonSerializable(typeof(A))]
, the JSON source generator will stop crawling properties, and as a result the metadata for B
and derived types will not be generated. Thus, trying to deserialize A
will throw at runtime.
Regression?
Don't think so.
Known Workarounds
A workaround is to manually also specify [JsonSerializable(typeof(B))]
on the JSON context. This works, but is pretty error prone, as it requires consumers to not just annotate the types they want to deserialize on their end, but also all types that are skipped by the generator due to the internal implementation detail of how the JSON models are defined.
I can understand if this is by design to potentially save unnecessary metadata (say, if the custom converter never created that instance through a JSON serialization API, but rather manually through other means), but the resulting behavior is not ideal for consumers. Would it be possible for the contract author to somehow inform the generator that if someone marks the root type as being serialized, then the metadata for also X property (or properties) should be generated?
This would allow both to keep the current behavior and save metadata, while at the same time allowing authors of these contracts to make the behavior transparent and avoid nasty surprises on the consumer side.
For instance: it would be nice if you could add [JsonSerializable(typeof(B))]
over B
in this case so that the source generator could see that "ok this type has a custom serializer so I'll stop traversing the hierarchy but I'll keep gathering metadata for these additional types that you have specified, sure". Would that make sense? 🙂
cc. @eiriktsarpalis