Closed dlyz closed 1 year ago
Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis See info in area-owners.md if you want to be subscribed.
Author: | dlyz |
---|---|
Assignees: | - |
Labels: | `area-System.Text.Json`, `untriaged` |
Milestone: | - |
@dlyz do you have a repro project including a sample JSON payload that's failing, type graph, and the custom converters?
Per notes above, this doesn't seem like a regression (same behavior since .NET 5.0) so I'm marking 8.0 for now. FWIW we are considering a feature to provide custom async converters which could help with perf - https://github.com/dotnet/runtime/issues/63795.
This issue has been marked needs-author-action
and may be missing some important information.
@dlyz do you have a repro project including a sample JSON payload that's failing, type graph, and the custom converters?
Yes, I have it in the collapsed area under Reproduction Steps
in the original post. Tried not to bloat the post, but looks like I overdid it =)
Not a regression but needs a look in .NET 8.
Just wanted to say thanks to @dlyz - this issue and the benchmark code saved me a huge headache!
It seems that the serializer is failing to read ahead the entire value before passing to the custom converter in the context of collection. We should try to fix this.
I've pushed #89637 that should fix this for .NET 8. In the meantime you could work around the issue by using the JsonSerializer
methods instead in your custom converter:
class Converter : JsonConverter<MyClass>
{
public override MyClass? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var inner = JsonSerializer.Deserialize<InnerClass>(ref reader, options);
return new MyClass { Inner = inner };
}
public override void Write(Utf8JsonWriter writer, MyClass value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, value, options);
}
The above will ensure that the right initializations are performed for the Utf8JsonReader
that is being passed to the serializer.
Description
Key points:
JsonConverter
Read
method we callRead
of another converter acquired withJsonSerializerOptions.GetConverter(typeof(InnerClass))
InnerClass
is anObjectDefaultConverter
InnerClass
with properties that are not represented in the InnerClass (excessive properties).Reproduction Steps
Minimal repro test
```cs using System; using System.IO; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using Xunit; public class NestedConverterTest { [JsonConverter(typeof(Converter))] private class MyClass { public InnerClass? Inner { get; set; } } private class InnerClass { public string? Value { get; set; } } private class Converter : JsonConverterExpected behavior
Should successfully deserialize as it does with smaller input JSON or when there are no excessive properties in JSON.
Actual behavior
For .NET 6:
Regression?
No response
Known Workarounds
The workaround is to use
JsonSerializer.Deserialize
instead of acquiring a converter fromJsonSerializerOptions
and callingJsonConverter.Read
, but this workaround may be significantly slower (see the benchmark below). Workaround may be applied conditionally whenUtf8JsonReader.IsFinalBlock
isfalse
.Configuration
Reproduces from .NET 5 to .NET 7 preview 7. Older versions don't work either, but for other reasons.
Other information
Custom converters are always called with full current JSON entity buffered (see https://github.com/dotnet/runtime/issues/39795#issuecomment-663082176), and looks like
ObjectDefaultConverter
is aware of that and chooses "fast path" (if (!state.SupportContinuation && !state.Current.CanContainMetadata)
), butUtf8JsonReader.Skip
method fails anyway because it checks for_isFinalBlock
which isfalse
.I think that this use case (to deserialize a part of JSON entity with default converter, inside another custom converter) is quite usual (for example I use it in my custom "known types" converter, tuple converter and others). The question is why not to use
JsonSerializer.Deserialize
. The answer is in the benchmark below.ReadCvtRead
(with nested converter call) is about 1.5 times faster thenReadDeserialize
(withJsonSerializer.Deserialize
call).Benchmark code
```cs using System; using System.Text.Json.Serialization; using System.Text.Json; using BenchmarkDotNet.Attributes; public class NestedConverterBenchmark { [Benchmark] public string WriteCvtWrite() { return JsonSerializer.Serialize(_model, _optionsCvt); } [Benchmark] public string WriteSerialize() { return JsonSerializer.Serialize(_model, _optionsSerializer); } [Benchmark] public MyClass