Commit 34c3fee
Fix JsonSerializer.Serialize producing invalid JSON for [JsonExtensionData] with JsonObject (#122838)
# Description
`JsonSerializer.Serialize` was producing invalid JSON when serializing
objects with `[JsonExtensionData]` properties of type `JsonObject`. The
output was missing the property name for extension data entries:
```csharp
var mix = new Mix { Id = 1, Extra = new JsonObject { ["nested"] = true } };
JsonSerializer.Serialize(mix);
// Before: {"Id":1,{"nested":true}} ← invalid JSON
// After: {"Id":1,"nested":true} ← correct
```
**Root cause**: `TryWriteDataExtensionProperty` in `JsonConverterOfT.cs`
called `TryWrite` for `JsonObject`, which writes the full object
including braces. Extension data should write only the contents
(key-value pairs) without wrapping braces.
**Fix**:
- Added a new internal `WriteContentsTo` method to `JsonObject` that
writes properties without wrapping braces, handling both
dictionary-backed and `JsonElement`-backed cases with proper
optimization.
- Added a `WriteExtensionDataValue` virtual method to `JsonConverter<T>`
with a generic `T value` parameter that throws by default, overridden in
`JsonObjectConverter` to call `WriteContentsTo`. This follows the same
pattern as `ReadElementAndSetProperty` and avoids direct type references
to `JsonObject` in `JsonConverterOfT.cs` to maintain trimming support.
- Updated `TryWriteDataExtensionProperty` to call the virtual method
instead of directly referencing `JsonObject`.
- Retained a `Debug.Assert` for the `JsonObject` type check using a
qualified `Nodes.JsonObject` reference (debug assertions are removed in
release builds, so this doesn't affect trimming).
# Customer Impact
Serialization of any class with `[JsonExtensionData]` of type
`JsonObject` produces malformed JSON that cannot be parsed.
# Regression
No. Reproducible from .NET 6 through .NET 8+. Deserialization works
correctly; only serialization was affected.
# Testing
- Added 4 targeted tests covering: valid JSON output, round-trip, empty
`JsonObject`, and null `JsonObject`
- All 49,808 existing `System.Text.Json.Tests` pass
- All 7,626 source generation tests pass
- Registered new test class in source generator contexts
(`ExtensionDataTestsContext_Metadata` and
`ExtensionDataTestsContext_Default`)
# Risk
Low. Change is minimal and isolated to the `JsonObject` branch of
extension data serialization. Dictionary extension data path is
unchanged. The virtual method pattern maintains trimming support by
avoiding direct type references in non-debug code paths.
# Package authoring no longer needed in .NET 9
IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet
package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older
versions.
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
>
> ----
>
> *This section details on the original issue you should resolve*
>
> <issue_title>JsonSerializer.Serialize produces invalid JSON for
[JsonExtensionData] property</issue_title>
> <issue_description>### Description
>
> When serializing object containing property marked with
`[JsonExtensionData]` attribute, serializer produces invalid JSON like
this one: `{"Id":1,{"nested":true}}`
>
> ### Reproduction Steps
>
> Run the following code
>
> ```C#
> var mix = new Mix
> {
> Id = 1,
> Extra = new() { ["nested"] = true, }
> };
>
> var text = System.Text.Json.JsonSerializer.Serialize(mix);
> Console.WriteLine(text);
> // output {"Id":1,{"nested":true}}
>
>
> public class Mix
> {
> public int Id { get; set; }
>
> [System.Text.Json.Serialization.JsonExtensionData]
> public System.Text.Json.Nodes.JsonObject? Extra { get; set; }
> }
>
> ```
>
> ### Expected behavior
>
> Correct JSON like `{"Id":1,"nested":true}` or at least valid JSON as
if there was not `[JsonExtensionData]` attribute
(`{"Id":1,"Extra":{"nested":true}}` ).
>
> ### Actual behavior
>
> Invalid JSON `{"Id":1,{"nested":true}}`
>
> ### Regression?
>
> Reproducible at least on .Net 6 to .Net 8
>
> ### Known Workarounds
>
> _No response_
>
> ### Configuration
>
> .Net 8
> Windows 11
> x64
>
> ### Other information
>
> _No response_</issue_description>
>
> ## Comments on the Issue (you are @copilot in this section)
>
> <comments>
> <comment_new><author>@</author><body>
> Tagging subscribers to this area: @dotnet/area-system-text-json,
@gregsdennis
> See info in
[area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md)
if you want to be subscribed.
> <details>
> <summary>Issue Details</summary>
> <hr />
>
> ### Description
>
> When serializing object containing property marked with
`[JsonExtensionData]` attribute, serializer produces invalid JSON like
this one: `{"Id":1,{"nested":true}}`
>
> ### Reproduction Steps
>
> Run the following code
>
> ```C#
> var mix = new Mix
> {
> Id = 1,
> Extra = new() { ["nested"] = true, }
> };
>
> var text = System.Text.Json.JsonSerializer.Serialize(mix);
> Console.WriteLine(text);
> // output {"Id":1,{"nested":true}}
>
>
> public class Mix
> {
> public int Id { get; set; }
>
> [System.Text.Json.Serialization.JsonExtensionData]
> public System.Text.Json.Nodes.JsonObject? Extra { get; set; }
> }
>
> ```
>
> ### Expected behavior
>
> Correct JSON like `{"Id":1,"nested":true}` or at least valid JSON as
if there was not `[JsonExtensionData]` attribute
(`{"Id":1,"Extra":{"nested":true}}` ).
>
> ### Actual behavior
>
> Invalid JSON `{"Id":1,{"nested":true}}`
>
> ### Regression?
>
> Reproducible at least on .Net 6 to .Net 8
>
> ### Known Workarounds
>
> _No response_
>
> ### Configuration
>
> .Net 8
> Windows 11
> x64
>
> ### Other information
>
> _No response_
>
> <table>
> <tr>
> <th align="left">Author:</th>
> <td>KalininAndreyVictorovich</td>
> </tr>
> <tr>
> <th align="left">Assignees:</th>
> <td>-</td>
> </tr>
> <tr>
> <th align="left">Labels:</th>
> <td>
>
> `area-System.Text.Json`
>
> </td>
> </tr>
> <tr>
> <th align="left">Milestone:</th>
> <td>-</td>
> </tr>
> </table>
> </details></body></comment_new>
> <comment_new><author>@eiriktsarpalis</author><body>
> Can confirm that this occurs. It seems we never added testing for the
serialization scenario -- deserialization appears to be working as
expected.</body></comment_new>
> <comment_new><author>@eiriktsarpalis</author><body>
> The documentation is correctly stating that `JsonObject` is one of the
supported types. However there is a bug specifically impacting
serialization for the particular type (FWIW `JsonExtensionData` is a
feature primarily oriented towards dserialization).</body></comment_new>
> <comment_new><author>@eiriktsarpalis</author><body>
> You're right, I misread the documentation which appears to be out of
date. The correct statement on supported types can actually be found in
the error messages of the implementation itself:
>
>
https://github.com/dotnet/runtime/blob/2d751cac7311b344c237df3b3c63b33434b52217/src/libraries/System.Text.Json/gen/Resources/Strings.resx#L150-L152
>
> In other words, what I mentioned earlier holds. `JsonObject` _is_
supported and there is a bug specifically concerning
serialization.</body></comment_new>
> <comment_new><author>@eiriktsarpalis</author><body>
> It would help if you could file a separate issue in dotnet-api-docs.
Thanks!</body></comment_new>
> </comments>
>
</details>
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes #97225
<!-- START COPILOT CODING AGENT TIPS -->
---
✨ Let Copilot coding agent [set things up for
you](https://github.com/dotnet/runtime/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>1 parent 1492e43 commit 34c3fee
5 files changed
Lines changed: 142 additions & 3 deletions
File tree
- src/libraries/System.Text.Json
- src/System/Text/Json
- Nodes
- Serialization
- Converters/Node
- tests
- Common
- System.Text.Json.SourceGeneration.Tests/Serialization
Lines changed: 23 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
175 | 179 | | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
176 | 199 | | |
177 | 200 | | |
178 | 201 | | |
| |||
186 | 209 | | |
187 | 210 | | |
188 | 211 | | |
189 | | - | |
190 | | - | |
191 | 212 | | |
192 | 213 | | |
193 | 214 | | |
| |||
Lines changed: 6 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
44 | 50 | | |
45 | 51 | | |
46 | 52 | | |
| |||
Lines changed: 17 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
458 | 458 | | |
459 | 459 | | |
460 | 460 | | |
| 461 | + | |
461 | 462 | | |
462 | | - | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
463 | 466 | | |
464 | 467 | | |
465 | 468 | | |
| |||
493 | 496 | | |
494 | 497 | | |
495 | 498 | | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
496 | 512 | | |
497 | 513 | | |
498 | 514 | | |
| |||
Lines changed: 94 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
815 | 815 | | |
816 | 816 | | |
817 | 817 | | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
818 | 912 | | |
819 | 913 | | |
820 | 914 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
77 | 78 | | |
78 | 79 | | |
79 | 80 | | |
| |||
144 | 145 | | |
145 | 146 | | |
146 | 147 | | |
| 148 | + | |
147 | 149 | | |
148 | 150 | | |
149 | 151 | | |
| |||
0 commit comments