Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Converters;
using System.Text.Json.Serialization.Metadata;

Expand Down Expand Up @@ -458,8 +460,27 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json
{
// If not JsonDictionaryConverter<T> 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);
Debug.Assert(Type == typeof(JsonObject));

// Write the JsonObject extension data contents without the wrapping braces.
// This is necessary because extension data properties should be flattened
// into the parent object, not nested as a separate object.
JsonObject jsonObject = (JsonObject)(object)value!;
foreach (KeyValuePair<string, JsonNode?> entry in jsonObject)
{
writer.WritePropertyName(entry.Key);

if (entry.Value is null)
{
writer.WriteNullValue();
}
else
{
entry.Value.WriteTo(writer, options);
}
}

return true;
}

if (writer.CurrentDepth >= options.EffectiveMaxDepth)
Expand Down
96 changes: 96 additions & 0 deletions src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,102 @@ public async Task DeserializeIntoJsonObjectProperty()
Assert.Equal(1, obj.MyOverflow["MyDict"]["Property1"].GetValue<int>());
}

[Fact]
public async Task SerializeJsonObjectExtensionData_ProducesValidJson()
{
// Regression test for https://github.com/dotnet/runtime/issues/115555
// 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()
{
// Regression test for https://github.com/dotnet/runtime/issues/115555
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<ClassWithJsonObjectExtensionDataAndProperty>(json);

Assert.Equal(original.Id, deserialized.Id);
Assert.NotNull(deserialized.Extra);
Assert.Equal(4, deserialized.Extra.Count);
Assert.Equal("value", deserialized.Extra["string"]!.GetValue<string>());
Assert.Equal(123, deserialized.Extra["number"]!.GetValue<int>());
Assert.False(deserialized.Extra["boolean"]!.GetValue<bool>());
Assert.Equal("data", deserialized.Extra["nested"]!["inner"]!.GetValue<string>());
}

[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")]
Expand Down
Loading