From 53fad007a933ccebd0a2a77bbb69e6b84ebc5aeb Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 24 Mar 2026 11:35:29 +0200 Subject: [PATCH 1/4] Add Utf8JsonWriter.Reset overloads accepting JsonWriterOptions Add two new public Reset overloads to Utf8JsonWriter: - Reset(IBufferWriter bufferWriter, JsonWriterOptions options) - Reset(Stream utf8Json, JsonWriterOptions options) These enable pooling scenarios where both the output destination and writer options need to change between reuses. The existing internal Reset(IBufferWriter, JsonWriterOptions) method used by Utf8JsonWriterCache is renamed to ConfigureForCacheReuse to avoid signature collision with the new public API. Prototype for: https://github.com/dotnet/runtime/issues/123221 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Text.Json/ref/System.Text.Json.cs | 2 + .../System/Text/Json/Writer/Utf8JsonWriter.cs | 72 +++++++- .../Text/Json/Writer/Utf8JsonWriterCache.cs | 4 +- .../Utf8JsonWriterTests.cs | 154 ++++++++++++++++++ 4 files changed, 229 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index c014669fd41b4b..07091b502b1143 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -587,7 +587,9 @@ public void Flush() { } public System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public void Reset() { } public void Reset(System.Buffers.IBufferWriter bufferWriter) { } + public void Reset(System.Buffers.IBufferWriter bufferWriter, System.Text.Json.JsonWriterOptions options) { } public void Reset(System.IO.Stream utf8Json) { } + public void Reset(System.IO.Stream utf8Json, System.Text.Json.JsonWriterOptions options) { } public void WriteBase64String(System.ReadOnlySpan utf8PropertyName, System.ReadOnlySpan bytes) { } public void WriteBase64String(System.ReadOnlySpan propertyName, System.ReadOnlySpan bytes) { } public void WriteBase64String(string propertyName, System.ReadOnlySpan bytes) { } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs index 6409daf43381d0..3934a99dec6641 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs @@ -297,9 +297,14 @@ public void Reset(Stream utf8Json) CheckNotDisposed(); if (utf8Json == null) + { throw new ArgumentNullException(nameof(utf8Json)); + } + if (!utf8Json.CanWrite) + { throw new ArgumentException(SR.StreamNotWritable); + } _stream = utf8Json; if (_arrayBufferWriter == null) @@ -310,8 +315,49 @@ public void Reset(Stream utf8Json) { _arrayBufferWriter.Clear(); } + _output = null; + ResetHelper(); + } + + /// + /// Resets the internal state so that it can be re-used with the new instance of + /// and the specified . + /// + /// An instance of used as a destination for writing JSON text into. + /// Defines the customized behavior of the . + /// + /// Thrown when the instance of that is passed in is null. + /// + /// + /// The instance of has been disposed. + /// + public void Reset(Stream utf8Json, JsonWriterOptions options) + { + CheckNotDisposed(); + + if (utf8Json == null) + { + throw new ArgumentNullException(nameof(utf8Json)); + } + + if (!utf8Json.CanWrite) + { + throw new ArgumentException(SR.StreamNotWritable); + } + + _stream = utf8Json; + if (_arrayBufferWriter == null) + { + _arrayBufferWriter = new ArrayBufferWriter(); + } + else + { + _arrayBufferWriter.Clear(); + } + _output = null; + SetOptions(options); ResetHelper(); } @@ -340,6 +386,30 @@ public void Reset(IBufferWriter bufferWriter) ResetHelper(); } + /// + /// Resets the internal state so that it can be re-used with the new instance of + /// and the specified . + /// + /// An instance of used as a destination for writing JSON text into. + /// Defines the customized behavior of the . + /// + /// Thrown when the instance of that is passed in is null. + /// + /// + /// The instance of has been disposed. + /// + public void Reset(IBufferWriter bufferWriter, JsonWriterOptions options) + { + CheckNotDisposed(); + + _output = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); + _stream = null; + _arrayBufferWriter = null; + + SetOptions(options); + ResetHelper(); + } + internal void ResetAllStateForCacheReuse() { ResetHelper(); @@ -349,7 +419,7 @@ internal void ResetAllStateForCacheReuse() _output = null; } - internal void Reset(IBufferWriter bufferWriter, JsonWriterOptions options) + internal void ConfigureForCacheReuse(IBufferWriter bufferWriter, JsonWriterOptions options) { Debug.Assert(_output is null && _stream is null && _arrayBufferWriter is null); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriterCache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriterCache.cs index bfe92487f01c8f..47a6f46d7be2b6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriterCache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriterCache.cs @@ -29,7 +29,7 @@ public static Utf8JsonWriter RentWriterAndBuffer(JsonWriterOptions options, int writer = state.Writer; bufferWriter.InitializeEmptyInstance(defaultBufferSize); - writer.Reset(bufferWriter, options); + writer.ConfigureForCacheReuse(bufferWriter, options); } else { @@ -50,7 +50,7 @@ public static Utf8JsonWriter RentWriter(JsonSerializerOptions options, IBufferWr { // First JsonSerializer call in the stack -- initialize & return the cached instance. writer = state.Writer; - writer.Reset(bufferWriter, options.GetWriterOptions()); + writer.ConfigureForCacheReuse(bufferWriter, options.GetWriterOptions()); } else { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs index 8d716b3ba089c0..1ee5774cc1f3ef 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs @@ -1180,18 +1180,168 @@ public void InvalidReset(JsonWriterOptions options) Assert.Throws(() => writeToStream.Reset((Stream)null)); Assert.Throws(() => writeToStream.Reset((IBufferWriter)null)); + Assert.Throws(() => writeToStream.Reset((Stream)null, options)); + Assert.Throws(() => writeToStream.Reset((IBufferWriter)null, options)); stream.Dispose(); Assert.Throws(() => writeToStream.Reset(stream)); + Assert.Throws(() => writeToStream.Reset(stream, options)); var output = new FixedSizedBufferWriter(256); using var writeToIBW = new Utf8JsonWriter(output, options); Assert.Throws(() => writeToIBW.Reset((Stream)null)); Assert.Throws(() => writeToIBW.Reset((IBufferWriter)null)); + Assert.Throws(() => writeToIBW.Reset((Stream)null, options)); + Assert.Throws(() => writeToIBW.Reset((IBufferWriter)null, options)); Assert.Throws(() => writeToIBW.Reset(stream)); + Assert.Throws(() => writeToIBW.Reset(stream, options)); + } + + [Theory] + [MemberData(nameof(JsonOptions_TestData))] + public void ResetWithNewOptions(JsonWriterOptions options) + { + var newOptions = new JsonWriterOptions + { + Indented = !options.Indented, + SkipValidation = !options.SkipValidation, + MaxDepth = 500, + IndentCharacter = '\t', + IndentSize = 4, + }; + + var stream = new MemoryStream(); + using var writeToStream = new Utf8JsonWriter(stream, options); + writeToStream.WriteNumberValue(1); + writeToStream.Flush(); + + Assert.True(writeToStream.BytesCommitted != 0); + + writeToStream.Reset(stream, newOptions); + Assert.Equal(0, writeToStream.BytesCommitted); + Assert.Equal(0, writeToStream.BytesPending); + Assert.Equal(0, writeToStream.CurrentDepth); + Assert.Equal(newOptions.Indented, writeToStream.Options.Indented); + Assert.Equal(newOptions.SkipValidation, writeToStream.Options.SkipValidation); + Assert.Equal(500, writeToStream.Options.MaxDepth); + Assert.Equal('\t', writeToStream.Options.IndentCharacter); + Assert.Equal(4, writeToStream.Options.IndentSize); + + long previousWritten = stream.Position; + writeToStream.Flush(); + Assert.Equal(previousWritten, stream.Position); + + writeToStream.WriteNumberValue(1); + writeToStream.Flush(); + + Assert.NotEqual(previousWritten, stream.Position); + + var output = new FixedSizedBufferWriter(257); + using var writeToIBW = new Utf8JsonWriter(output, options); + writeToIBW.WriteNumberValue(1); + writeToIBW.Flush(); + + Assert.True(writeToIBW.BytesCommitted != 0); + + writeToIBW.Reset(output, newOptions); + Assert.Equal(0, writeToIBW.BytesCommitted); + Assert.Equal(0, writeToIBW.BytesPending); + Assert.Equal(0, writeToIBW.CurrentDepth); + Assert.Equal(newOptions.Indented, writeToIBW.Options.Indented); + Assert.Equal(newOptions.SkipValidation, writeToIBW.Options.SkipValidation); + Assert.Equal(500, writeToIBW.Options.MaxDepth); + Assert.Equal('\t', writeToIBW.Options.IndentCharacter); + Assert.Equal(4, writeToIBW.Options.IndentSize); + + previousWritten = output.FormattedCount; + writeToIBW.Flush(); + Assert.Equal(previousWritten, output.FormattedCount); + + writeToIBW.WriteNumberValue(1); + writeToIBW.Flush(); + + Assert.NotEqual(previousWritten, output.FormattedCount); + } + + [Theory] + [MemberData(nameof(JsonOptions_TestData))] + public void ResetChangeOutputModeWithNewOptions(JsonWriterOptions options) + { + var newOptions = new JsonWriterOptions + { + Indented = !options.Indented, + SkipValidation = !options.SkipValidation, + }; + + var stream = new MemoryStream(); + using var writeToStream = new Utf8JsonWriter(stream, options); + writeToStream.WriteNumberValue(1); + writeToStream.Flush(); + + Assert.True(writeToStream.BytesCommitted != 0); + + var output = new FixedSizedBufferWriter(256); + writeToStream.Reset(output, newOptions); + Assert.Equal(0, writeToStream.BytesCommitted); + Assert.Equal(0, writeToStream.BytesPending); + Assert.Equal(0, writeToStream.CurrentDepth); + Assert.Equal(newOptions.Indented, writeToStream.Options.Indented); + Assert.Equal(newOptions.SkipValidation, writeToStream.Options.SkipValidation); + + writeToStream.WriteNumberValue(1); + writeToStream.Flush(); + Assert.True(writeToStream.BytesCommitted != 0); + Assert.True(output.FormattedCount != 0); + + output = new FixedSizedBufferWriter(256); + using var writeToIBW = new Utf8JsonWriter(output, options); + writeToIBW.WriteNumberValue(1); + writeToIBW.Flush(); + + Assert.True(writeToIBW.BytesCommitted != 0); + + stream = new MemoryStream(); + writeToIBW.Reset(stream, newOptions); + Assert.Equal(0, writeToIBW.BytesCommitted); + Assert.Equal(0, writeToIBW.BytesPending); + Assert.Equal(0, writeToIBW.CurrentDepth); + Assert.Equal(newOptions.Indented, writeToIBW.Options.Indented); + Assert.Equal(newOptions.SkipValidation, writeToIBW.Options.SkipValidation); + + writeToIBW.WriteNumberValue(1); + writeToIBW.Flush(); + Assert.True(writeToIBW.BytesCommitted != 0); + Assert.True(stream.Position != 0); + } + + [Fact] + public void ResetWithNewOptions_ProducesExpectedOutput() + { + var output = new ArrayBufferWriter(); + using var writer = new Utf8JsonWriter(output, new JsonWriterOptions { Indented = false }); + + writer.WriteStartObject(); + writer.WriteNumber("x", 1); + writer.WriteEndObject(); + writer.Flush(); + + string compact = Encoding.UTF8.GetString(output.WrittenSpan.ToArray()); + Assert.Equal("""{"x":1}""", compact); + + output.Clear(); + writer.Reset(output, new JsonWriterOptions { Indented = true }); + + writer.WriteStartObject(); + writer.WriteNumber("x", 1); + writer.WriteEndObject(); + writer.Flush(); + + string indented = Encoding.UTF8.GetString(output.WrittenSpan.ToArray()); + Assert.Contains("\n", indented); + Assert.Contains("\"x\": 1", indented); } [Theory] @@ -1381,6 +1531,7 @@ public void UseAfterDisposeInvalid(JsonWriterOptions options) var stream = new MemoryStream(); Assert.Throws(() => jsonUtf8.Reset(stream)); + Assert.Throws(() => jsonUtf8.Reset(stream, options)); using var writeToStream = new Utf8JsonWriter(stream, options); writeToStream.WriteStartObject(); @@ -1398,6 +1549,7 @@ public void UseAfterDisposeInvalid(JsonWriterOptions options) Assert.Throws(() => writeToStream.Reset()); Assert.Throws(() => jsonUtf8.Reset(output)); + Assert.Throws(() => jsonUtf8.Reset(output, options)); } [Theory] @@ -1423,6 +1575,7 @@ public async Task UseAfterDisposeInvalidAsync(JsonWriterOptions options) var stream = new MemoryStream(); Assert.Throws(() => jsonUtf8.Reset(stream)); + Assert.Throws(() => jsonUtf8.Reset(stream, options)); using var writeToStream = new Utf8JsonWriter(stream, options); writeToStream.WriteStartObject(); @@ -1440,6 +1593,7 @@ public async Task UseAfterDisposeInvalidAsync(JsonWriterOptions options) Assert.Throws(() => writeToStream.Reset()); Assert.Throws(() => jsonUtf8.Reset(output)); + Assert.Throws(() => jsonUtf8.Reset(output, options)); } [Theory] From c2284d31d0c1edf16303715508d5f4e2f691fe3e Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 6 Apr 2026 20:04:25 +0300 Subject: [PATCH 2/4] Add regression test for disposed cached Utf8JsonWriter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Utf8JsonWriterTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs index 1ee5774cc1f3ef..b545496857b71d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs @@ -1200,6 +1200,16 @@ public void InvalidReset(JsonWriterOptions options) Assert.Throws(() => writeToIBW.Reset(stream, options)); } + [Fact] + public static void JsonSerializerCacheNotPoisonedByDisposedWriter() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new DisposingInt32Converter()); + + Assert.Throws(() => JsonSerializer.Serialize(1, options)); + Assert.Equal("2", JsonSerializer.Serialize(2)); + } + [Theory] [MemberData(nameof(JsonOptions_TestData))] public void ResetWithNewOptions(JsonWriterOptions options) @@ -1266,6 +1276,18 @@ public void ResetWithNewOptions(JsonWriterOptions options) Assert.NotEqual(previousWritten, output.FormattedCount); } + private sealed class DisposingInt32Converter : System.Text.Json.Serialization.JsonConverter + { + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotSupportedException(); + + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + { + writer.Dispose(); + throw new InvalidOperationException(); + } + } + [Theory] [MemberData(nameof(JsonOptions_TestData))] public void ResetChangeOutputModeWithNewOptions(JsonWriterOptions options) From 446b6184a46bc3fafe317db32e1c3f678d896cfc Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 6 Apr 2026 20:24:13 +0300 Subject: [PATCH 3/4] Consolidate Utf8JsonWriter reset overloads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System/Text/Json/Writer/Utf8JsonWriter.cs | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs index 3934a99dec6641..b0c9e7b808a66d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs @@ -334,31 +334,8 @@ public void Reset(Stream utf8Json) /// public void Reset(Stream utf8Json, JsonWriterOptions options) { - CheckNotDisposed(); - - if (utf8Json == null) - { - throw new ArgumentNullException(nameof(utf8Json)); - } - - if (!utf8Json.CanWrite) - { - throw new ArgumentException(SR.StreamNotWritable); - } - - _stream = utf8Json; - if (_arrayBufferWriter == null) - { - _arrayBufferWriter = new ArrayBufferWriter(); - } - else - { - _arrayBufferWriter.Clear(); - } - - _output = null; + Reset(utf8Json); SetOptions(options); - ResetHelper(); } /// @@ -400,14 +377,8 @@ public void Reset(IBufferWriter bufferWriter) /// public void Reset(IBufferWriter bufferWriter, JsonWriterOptions options) { - CheckNotDisposed(); - - _output = bufferWriter ?? throw new ArgumentNullException(nameof(bufferWriter)); - _stream = null; - _arrayBufferWriter = null; - + Reset(bufferWriter); SetOptions(options); - ResetHelper(); } internal void ResetAllStateForCacheReuse() From 613b852496beeddf3e3f6d34e8b1b411c841c322 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 7 Apr 2026 10:50:20 +0300 Subject: [PATCH 4/4] Document Utf8JsonWriter reset exceptions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Text/Json/Writer/Utf8JsonWriter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs index b0c9e7b808a66d..d87f5fbc771332 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.cs @@ -289,6 +289,9 @@ public void Reset() /// /// Thrown when the instance of that is passed in is null. /// + /// + /// Thrown when the instance of that is passed in does not support writing. + /// /// /// The instance of has been disposed. /// @@ -329,6 +332,9 @@ public void Reset(Stream utf8Json) /// /// Thrown when the instance of that is passed in is null. /// + /// + /// Thrown when the instance of that is passed in does not support writing. + /// /// /// The instance of has been disposed. ///