-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
System.Text.Json custom converter for Dictionary<string, object> collides with JsonExtensionData #60560
Comments
Tagging @dotnet/area-system-text-json since the bot didn't. |
Can reproduce, minimal repro: using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
var value = new MyClass { ExtData = { ["Hello"] = "World" } };
var settings = new JsonSerializerOptions { Converters = { new CustomDictionaryConverter() } };
string json = JsonSerializer.Serialize(value, settings);
Console.WriteLine(json); // {42}
public class MyClass
{
[JsonExtensionData]
public Dictionary<string, object> ExtData { get; set; } = new Dictionary<string, object>();
}
public class CustomDictionaryConverter : JsonConverter<Dictionary<string, object?>>
{
public override Dictionary<string, object?> Read(ref Utf8JsonReader reader, Type? typeToConvert, JsonSerializerOptions options)
=> throw new NotSupportedException();
public override void Write(Utf8JsonWriter writer, Dictionary<string, object?> values, JsonSerializerOptions options)
=> writer.WriteNumberValue(42);
} This is a bug, I think we need to make |
As shown in #60806 this also happens when using Given that it is apparently not allowed to add a custom converter to the |
Minimal reproduction using using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
Person value = new() { Extensions = new() { ["Key"] = 42 } };
Console.WriteLine(JsonSerializer.Serialize(value));
public class Person
{
[JsonExtensionData]
public JsonObject? Extensions { get; set; }
} |
Upon closer inspection, it appears that this is by-design behavior. Registering custom converters for extension data properties is a supported scenario, but converters need to be written in a way that accounts for the fact that the types are being used as extension data: runtime/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs Lines 366 to 403 in cd75ae1
Writing converters in this way violates the invariants of My recommendation would be to introduce a breaking change here, I don't see real value in customizing extension data serialization and the current behavior sets up users for emitting invalid JSON, even though they might have never intended to customize extension data serialization. @krwq @steveharter thoughts? |
@eiriktsarpalis I agree extension data properties should not use global converters (whatever is set on options or type) but it should still respect any |
The problem is that it cannot do that unless the converter has been written in way such that properties are emitted without any curly braces (see example above). It's a pit of failure that breaks the composition model for converters (hence the invalid JSON being reported in this issue). I think we should consider making a breaking change in the future. |
Just wanted to say, using Supporting unit test: runtime/src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs Lines 786 to 795 in b6115b0
|
@amis92 this strikes me as a separate issue. Can you please open a new one with this info, also describing the issues you're having with serialization? |
Just having written code on net7 which overwrote some json files on which the reading app now crashes because they are invalid now due to this problem - now I remember having tried the same on net6 and got weird results, too; by the time thought I was using something in beta stage. Ok, what exactly tells me here that JsonObject cannot be used for serialization but only deserialization? // Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Text.Json.Serialization
{
/// <summary>
/// When placed on a property or field of type <see cref="System.Text.Json.Nodes.JsonObject"/> or
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, any properties that do not have a
/// matching property or field are added during deserialization and written during serialization.
/// </summary>
/// <remarks>
/// When using <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, the TKey value must be <see cref="string"/>
/// and TValue must be <see cref="JsonElement"/> or <see cref="object"/>.
///
/// During deserializing with a <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/> extension property with TValue as
/// <see cref="object"/>, the type of object created will either be a <see cref="System.Text.Json.Nodes.JsonNode"/> or a
/// <see cref="JsonElement"/> depending on the value of <see cref="System.Text.Json.JsonSerializerOptions.UnknownTypeHandling"/>.
///
/// If a <see cref="JsonElement"/> is created, a "null" JSON value is treated as a JsonElement with <see cref="JsonElement.ValueKind"/>
/// set to <see cref="JsonValueKind.Null"/>, otherwise a "null" JSON value is treated as a <c>null</c> object reference.
///
/// During serializing, the name of the extension data member is not included in the JSON;
/// the data contained within the extension data is serialized as properties of the JSON object.
///
/// If there is more than one extension member on a type, or the member is not of the correct type,
/// an <see cref="InvalidOperationException"/> is thrown during the first serialization or deserialization of that type.
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class JsonExtensionDataAttribute : JsonAttribute
{
}
} Doesn't look like "not documented" to me. |
It appears to be supported for serialization. What do you expect for the following? using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
public class Program
{
public static void Main()
{
JsonObject overflow = new JsonObject() { ["Hello"] = "World" };
ClassWithExtensionPropertyAsJsonObject obj = new() { MyOverflow = overflow };
string json = JsonSerializer.Serialize(overflow);
Console.WriteLine(json);
}
public class ClassWithExtensionPropertyAsJsonObject
{
[JsonExtensionData]
public JsonObject MyOverflow { get; set; }
}
} I expect |
I expect a Json serializer to generate json - and not something which looks like json but cannot be parsed by any json parser out in the world. But the latter is what happens. |
@layomia I believe it's the same issue. This one was opened specifically for custom converters for |
In general I think a separate, internal converter should be used to handle extension data. We could support a custom converter applied with [ |
Has there been any progress on this issue? Or even a work around for the time being? This has been open for a while, I hope there is still appetite to get this resolved. |
@lukecusolito it's been parked for a while since upon closer inspection this is by design, see #60560 (comment). I still think it needs to be fixed, but that would need to be flagged as a breaking change and we should be providing a workaround for users that do depend on the current behavior. It certainly won't be changed for .NET 8. |
The documentation should be updated to not say you can use JsonObject |
@eiriktsarpalis Look a good workaround would be fantastic if one can be provided. The system I am building is dependent on on dynamic data, this is the only piece of the puzzle that does not work. The current workaround I have in place is to serialize my JsonObject into a string and then deserialize to a Dictionary<string, JsonElement> then assigning to a JsonExtensionData property on my response class. It sucks, but it is 'temporary' until I come up with something better. The problem is from what I can see, I doesn't look like there is anyway to convert a JsonObject into either a JsonDocument or a JsonElement without first converting to and from a string due to their immutable nature. I am going to explore a JsonConverter next, if you have a good workaround I would be greatly appreciative. I will post what I have if I can get it to work. If support for this is dropped, then there needs to be an efficient way to convert to a JsonDocument or JsonElement, maybe some way to parse from a JsonObject |
Okay, I have 2 potential workarounds for anyone interested. Keeping in mind that my requirements only need me to write to an API response object so my samples are only going one way. Option 1 - Custom Converter public class CustomJsonObjectOverflowConverter : JsonConverter<JsonObject>
{
public override JsonObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, JsonObject value, JsonSerializerOptions options)
{
foreach (var item in value)
{
writer.WritePropertyName(item.Key);
item.Value.WriteTo(writer, options);
}
}
}
public class JsonObjectResponse
{
// JsonExtensionData attribute is still required. If not supplied the property name ("Value") still gets written to the response body
[JsonExtensionData]
[JsonConverter(typeof(CustomJsonObjectOverflowConverter))]
public JsonObject Value { get; set; }
...
} Option 2 - Convert type public class JsonObjectResponse
{
public JsonObjectResponse(JsonObject jsonObject)
{
Value = jsonObject.Deserialize<Dictionary<string, JsonElement>>();
}
[JsonExtensionData]
public Dictionary<string, JsonElement> Value { get; set; }
...
} I am interested in your thoughts on these workarounds. I feel option 2 is safer as it uses first party mechanisms but double handling the data might have an impact on performance |
Unfortunately this does not work for deserialization. |
same issue here with ProblemDetails retuned by asp.net |
Description
Implementing a custom converter for a Dictionary<string, object> is used to serialize JsonExtensionData properties but there is no way for the converter to know if it is serializing an extension property or a dictionary property. Because of that invalid JSON is generated.
Reproduction Steps
See fiddle: https://dotnetfiddle.net/VIrtyu
Expected behavior
The custom converter should have a way to check if the current write operation value is json extension data.
{"RegularData":{"World":"Hello"},"Hello":"World"}
Actual behavior
The custom converter does not know if it json extension data and therefore writes an object instead of weaving the properties into the parent.
{"RegularData":{"World":"Hello"},{"Hello":"World"}}
Regression?
No response
Known Workarounds
No response
Configuration
.NET 5, System.Text.Json 5.0.2
Other information
Might be related to #32903
The text was updated successfully, but these errors were encountered: