From 1395a469edb80bec568607c644855ea8a4166ce4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 13:27:13 +0000 Subject: [PATCH] Leave the writer empty when Newtonsoft TrySerialize fails [patch] Serialize into a local buffer and copy it to the caller's writer only on success. Disposing the JsonTextWriter auto-closes open objects, so a failure partway through left well-formed, truncated JSON behind. Fixes #154 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SETCEsuMvZFcF6qXRguNds --- .../NewtonsoftJsonSerializationProvider.cs | 17 ++++++++++++----- ...ewtonsoftJsonSerializationProviderTests.cs | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Essentials.SerializationProviders.NewtonsoftJson/NewtonsoftJsonSerializationProvider.cs b/Essentials.SerializationProviders.NewtonsoftJson/NewtonsoftJsonSerializationProvider.cs index 581d90b..4bea7a7 100644 --- a/Essentials.SerializationProviders.NewtonsoftJson/NewtonsoftJsonSerializationProvider.cs +++ b/Essentials.SerializationProviders.NewtonsoftJson/NewtonsoftJsonSerializationProvider.cs @@ -4,6 +4,7 @@ namespace ktsu.Essentials.SerializationProviders.NewtonsoftJson; using ktsu.Essentials; using System; +using System.Globalization; using System.IO; using System.Text; using Newtonsoft.Json; @@ -35,11 +36,17 @@ public bool TrySerialize(object obj, TextWriter writer) try { - // The caller owns the writer, so disposing the JSON writer must not close it. - using JsonTextWriter jsonWriter = new(writer) { CloseOutput = false }; - JsonSerializer serializer = JsonSerializer.Create(settings); - serializer.Serialize(jsonWriter, obj); - jsonWriter.Flush(); + // Serialize into a local buffer and copy it to the caller's writer only on success. + // Disposing a JsonTextWriter closes any object or array a failure left open, so + // streaming straight into the caller's writer would leave truncated JSON that parses. + using StringWriter buffer = new(CultureInfo.InvariantCulture); + using (JsonTextWriter jsonWriter = new(buffer)) + { + JsonSerializer serializer = JsonSerializer.Create(settings); + serializer.Serialize(jsonWriter, obj); + } + + writer.Write(buffer.ToString()); return true; } catch (JsonSerializationException) diff --git a/Essentials.Tests/NewtonsoftJsonSerializationProviderTests.cs b/Essentials.Tests/NewtonsoftJsonSerializationProviderTests.cs index ed39c5e..d5eea8a 100644 --- a/Essentials.Tests/NewtonsoftJsonSerializationProviderTests.cs +++ b/Essentials.Tests/NewtonsoftJsonSerializationProviderTests.cs @@ -28,6 +28,25 @@ public void TrySerialize_WriterThrowingIOException_ReturnsFalse() Assert.IsFalse(provider.TrySerialize(new { A = 1 }, writer)); } + [TestMethod] + public void TrySerialize_CyclicObject_ReturnsFalseAndWritesNothing() + { + NewtonsoftJsonSerializationProvider provider = new(); + Node node = new() { Name = "a" }; + node.Next = node; + using StringWriter writer = new(); + + Assert.IsFalse(provider.TrySerialize(node, writer)); + Assert.AreEqual(string.Empty, writer.ToString()); + } + + private sealed class Node + { + public string Name { get; set; } = string.Empty; + + public Node? Next { get; set; } + } + private sealed class FailingWriter : StringWriter { public override void Write(char value) => throw new IOException("disk full");