diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs
index 9cfe0b8cb8fc86..d83d6594e9da75 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs
@@ -172,7 +172,30 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
else
{
writer.WriteStartObject();
+ WriteContentsTo(writer, options);
+ writer.WriteEndObject();
+ }
+ }
+ ///
+ /// Writes the properties of this JsonObject to the writer without the surrounding braces.
+ /// This is used for extension data serialization where the properties should be flattened
+ /// into the parent object.
+ ///
+ internal void WriteContentsTo(Utf8JsonWriter writer, JsonSerializerOptions? options)
+ {
+ GetUnderlyingRepresentation(out OrderedDictionary? dictionary, out JsonElement? jsonElement);
+
+ if (dictionary is null && jsonElement.HasValue)
+ {
+ // Write properties from the underlying JsonElement without converting to nodes.
+ foreach (JsonProperty property in jsonElement.Value.EnumerateObject())
+ {
+ property.WriteTo(writer);
+ }
+ }
+ else
+ {
foreach (KeyValuePair entry in Dictionary)
{
writer.WritePropertyName(entry.Key);
@@ -186,8 +209,6 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio
entry.Value.WriteTo(writer, options);
}
}
-
- writer.WriteEndObject();
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs
index fb97edcf75f54d..7de235cef7aafe 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs
@@ -41,6 +41,15 @@ internal override void ReadElementAndSetProperty(
}
}
+ internal override void WriteExtensionDataValue(
+ Utf8JsonWriter writer,
+ object value,
+ JsonSerializerOptions options)
+ {
+ Debug.Assert(value is JsonObject);
+ ((JsonObject)value).WriteContentsTo(writer, options);
+ }
+
public override void Write(Utf8JsonWriter writer, JsonObject? value, JsonSerializerOptions options)
{
if (value is null)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs
index 1a0e007695dcfb..20b271d53e4b40 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs
@@ -110,6 +110,20 @@ internal virtual void ReadElementAndSetProperty(
throw new InvalidOperationException();
}
+ ///
+ /// Used to support JsonObject as an extension property in a loosely-typed, trimmable manner.
+ /// Writes the extension data contents without wrapping object braces.
+ ///
+ internal virtual void WriteExtensionDataValue(
+ Utf8JsonWriter writer,
+ object value,
+ JsonSerializerOptions options)
+ {
+ Debug.Fail("Should not be reachable.");
+
+ throw new InvalidOperationException();
+ }
+
internal virtual JsonTypeInfo CreateJsonTypeInfo(JsonSerializerOptions options)
{
Debug.Fail("Should not be reachable.");
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
index 3a9684f3fd86e9..bf2d5897ac9e5e 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;
@@ -458,8 +459,11 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json
{
// If not JsonDictionaryConverter then we are JsonObject.
// Avoid a type reference to JsonObject and its converter to support trimming.
- Debug.Assert(Type == typeof(Nodes.JsonObject));
- return TryWrite(writer, value, options, ref state);
+ // The WriteExtensionDataValue virtual method is overridden by the JsonObject converter.
+ Debug.Assert(Type == typeof(JsonObject));
+ WriteExtensionDataValue(writer, value!, options);
+
+ return true;
}
if (writer.CurrentDepth >= options.EffectiveMaxDepth)
diff --git a/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs
index 570624a2a229df..ed8297a2decab0 100644
--- a/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs
+++ b/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs
@@ -815,6 +815,100 @@ public async Task DeserializeIntoJsonObjectProperty()
Assert.Equal(1, obj.MyOverflow["MyDict"]["Property1"].GetValue());
}
+ [Fact]
+ public async Task SerializeJsonObjectExtensionData_ProducesValidJson()
+ {
+ // JsonSerializer.Serialize was producing invalid JSON for [JsonExtensionData] property of type JsonObject
+ // Output was: {"Id":1,{"nested":true}} instead of {"Id":1,"nested":true}
+
+ var obj = new ClassWithJsonObjectExtensionDataAndProperty
+ {
+ Id = 1,
+ Extra = new JsonObject { ["nested"] = true }
+ };
+
+ string json = await Serializer.SerializeWrapper(obj);
+
+ // Verify the JSON is valid by parsing it
+ using JsonDocument doc = JsonDocument.Parse(json);
+
+ // Verify the structure is correct: extension data should be flattened, not nested
+ Assert.True(doc.RootElement.TryGetProperty("Id", out JsonElement idElement));
+ Assert.Equal(1, idElement.GetInt32());
+
+ Assert.True(doc.RootElement.TryGetProperty("nested", out JsonElement nestedElement));
+ Assert.True(nestedElement.GetBoolean());
+
+ // Extension data property name should NOT appear in the output
+ Assert.False(doc.RootElement.TryGetProperty("Extra", out _));
+
+ // Verify expected JSON format
+ Assert.Equal(@"{""Id"":1,""nested"":true}", json);
+ }
+
+ [Fact]
+ public async Task SerializeJsonObjectExtensionData_RoundTrip()
+ {
+ var original = new ClassWithJsonObjectExtensionDataAndProperty
+ {
+ Id = 42,
+ Extra = new JsonObject
+ {
+ ["string"] = "value",
+ ["number"] = 123,
+ ["boolean"] = false,
+ ["nested"] = new JsonObject { ["inner"] = "data" }
+ }
+ };
+
+ string json = await Serializer.SerializeWrapper(original);
+
+ // Verify round-trip
+ var deserialized = await Serializer.DeserializeWrapper(json);
+
+ Assert.Equal(original.Id, deserialized.Id);
+ Assert.NotNull(deserialized.Extra);
+ Assert.Equal(4, deserialized.Extra.Count);
+ Assert.Equal("value", deserialized.Extra["string"]!.GetValue());
+ Assert.Equal(123, deserialized.Extra["number"]!.GetValue());
+ Assert.False(deserialized.Extra["boolean"]!.GetValue());
+ Assert.Equal("data", deserialized.Extra["nested"]!["inner"]!.GetValue());
+ }
+
+ [Fact]
+ public async Task SerializeJsonObjectExtensionData_EmptyJsonObject()
+ {
+ var obj = new ClassWithJsonObjectExtensionDataAndProperty
+ {
+ Id = 1,
+ Extra = new JsonObject()
+ };
+
+ string json = await Serializer.SerializeWrapper(obj);
+ Assert.Equal(@"{""Id"":1}", json);
+ }
+
+ [Fact]
+ public async Task SerializeJsonObjectExtensionData_NullJsonObject()
+ {
+ var obj = new ClassWithJsonObjectExtensionDataAndProperty
+ {
+ Id = 1,
+ Extra = null
+ };
+
+ string json = await Serializer.SerializeWrapper(obj);
+ Assert.Equal(@"{""Id"":1}", json);
+ }
+
+ public class ClassWithJsonObjectExtensionDataAndProperty
+ {
+ public int Id { get; set; }
+
+ [JsonExtensionData]
+ public JsonObject? Extra { get; set; }
+ }
+
[Fact]
#if BUILDING_SOURCE_GENERATOR_TESTS
[ActiveIssue("https://github.com/dotnet/runtime/issues/58945")]
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ExtensionDataTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ExtensionDataTests.cs
index 89829b304bf4b2..7b9d4f8ab2fd7c 100644
--- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ExtensionDataTests.cs
+++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ExtensionDataTests.cs
@@ -74,6 +74,7 @@ public ExtensionDataTests_Metadata()
[JsonSerializable(typeof(ClassWithIReadOnlyDictionaryExtensionPropertyAsJsonElementWithProperty))]
[JsonSerializable(typeof(ClassWithIReadOnlyDictionaryAlreadyInstantiated))]
[JsonSerializable(typeof(ClassWithIReadOnlyDictionaryJsonElementAlreadyInstantiated))]
+ [JsonSerializable(typeof(ClassWithJsonObjectExtensionDataAndProperty))]
internal sealed partial class ExtensionDataTestsContext_Metadata : JsonSerializerContext
{
}
@@ -144,6 +145,7 @@ public ExtensionDataTests_Default()
[JsonSerializable(typeof(ClassWithIReadOnlyDictionaryExtensionPropertyAsJsonElementWithProperty))]
[JsonSerializable(typeof(ClassWithIReadOnlyDictionaryAlreadyInstantiated))]
[JsonSerializable(typeof(ClassWithIReadOnlyDictionaryJsonElementAlreadyInstantiated))]
+ [JsonSerializable(typeof(ClassWithJsonObjectExtensionDataAndProperty))]
internal sealed partial class ExtensionDataTestsContext_Default : JsonSerializerContext
{
}