Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
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
/// When placed on a property or field of type <see cref="System.Text.Json.Nodes.JsonObject"/>,
/// <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/>, or
/// <see cref="System.Collections.Generic.IReadOnlyDictionary{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"/>
/// When using <see cref="System.Collections.Generic.IDictionary{TKey, TValue}"/> or
/// <see cref="System.Collections.Generic.IReadOnlyDictionary{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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ internal static void CreateExtensionDataProperty(
{
// Create the appropriate dictionary type. We already verified the types.
#if DEBUG
Type underlyingIDictionaryType = jsonPropertyInfo.PropertyType.GetCompatibleGenericInterface(typeof(IDictionary<,>))!;
Type? underlyingIDictionaryType = jsonPropertyInfo.PropertyType.GetCompatibleGenericInterface(typeof(IDictionary<,>))
?? jsonPropertyInfo.PropertyType.GetCompatibleGenericInterface(typeof(IReadOnlyDictionary<,>));
Debug.Assert(underlyingIDictionaryType is not null);
Type[] genericArgs = underlyingIDictionaryType.GetGenericArguments();

Debug.Assert(underlyingIDictionaryType.IsGenericType);
Expand All @@ -136,6 +138,25 @@ internal static void CreateExtensionDataProperty(
{
ThrowHelper.ThrowInvalidOperationException_NodeJsonObjectCustomConverterNotAllowedOnExtensionProperty();
}
// For IReadOnlyDictionary<string, object> or IReadOnlyDictionary<string, JsonElement> interface types,
// create a Dictionary<TKey, TValue> instance. We only do this if Dictionary can be assigned back
// to the property (i.e., the property is the interface type itself, not a concrete implementation).
else if (typeof(IReadOnlyDictionary<string, object>).IsAssignableFrom(jsonPropertyInfo.PropertyType) &&
jsonPropertyInfo.PropertyType.IsAssignableFrom(typeof(Dictionary<string, object>)))
{
extensionData = new Dictionary<string, object>();
Debug.Assert(jsonPropertyInfo.Set != null);
jsonPropertyInfo.Set(obj, extensionData);
return;
}
else if (typeof(IReadOnlyDictionary<string, JsonElement>).IsAssignableFrom(jsonPropertyInfo.PropertyType) &&
jsonPropertyInfo.PropertyType.IsAssignableFrom(typeof(Dictionary<string, JsonElement>)))
{
extensionData = new Dictionary<string, JsonElement>();
Debug.Assert(jsonPropertyInfo.Set != null);
jsonPropertyInfo.Set(obj, extensionData);
return;
}
else
{
ThrowHelper.ThrowNotSupportedException_SerializationNotSupported(jsonPropertyInfo.PropertyType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,11 @@ internal static bool IsValidExtensionDataProperty(Type propertyType)
{
return typeof(IDictionary<string, object>).IsAssignableFrom(propertyType) ||
typeof(IDictionary<string, JsonElement>).IsAssignableFrom(propertyType) ||
// IReadOnlyDictionary is supported only if a Dictionary can be assigned to it (e.g., the interface itself)
(typeof(IReadOnlyDictionary<string, object>).IsAssignableFrom(propertyType) &&
propertyType.IsAssignableFrom(typeof(Dictionary<string, object>))) ||
(typeof(IReadOnlyDictionary<string, JsonElement>).IsAssignableFrom(propertyType) &&
propertyType.IsAssignableFrom(typeof(Dictionary<string, JsonElement>))) ||
// Avoid a reference to typeof(JsonNode) to support trimming.
(propertyType.FullName == JsonObjectTypeName && ReferenceEquals(propertyType.Assembly, typeof(JsonTypeInfo).Assembly));
}
Expand Down
69 changes: 69 additions & 0 deletions src/libraries/System.Text.Json/tests/Common/ExtensionDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1483,5 +1483,74 @@ public class ClassWithEmptyPropertyNameAndExtensionProperty
[JsonExtensionData]
public IDictionary<string, JsonElement> MyOverflow { get; set; }
}

[Fact]
public async Task IReadOnlyDictionary_ObjectExtensionPropertyRoundTrip()
{
string json = @"{""MyIntMissing"":2, ""MyInt"":1}";
ClassWithIReadOnlyDictionaryExtensionPropertyAsObjectWithProperty obj = await Serializer.DeserializeWrapper<ClassWithIReadOnlyDictionaryExtensionPropertyAsObjectWithProperty>(json);

Assert.NotNull(obj.MyOverflow);
Assert.Equal(1, obj.MyInt);
Assert.IsType<JsonElement>(obj.MyOverflow["MyIntMissing"]);
Assert.Equal(2, ((JsonElement)obj.MyOverflow["MyIntMissing"]).GetInt32());

string jsonSerialized = await Serializer.SerializeWrapper(obj);
Assert.Contains("\"MyIntMissing\"", jsonSerialized);
Assert.Contains("\"MyInt\"", jsonSerialized);
Assert.DoesNotContain(nameof(ClassWithIReadOnlyDictionaryExtensionPropertyAsObjectWithProperty.MyOverflow), jsonSerialized);
}

[Fact]
public async Task IReadOnlyDictionary_JsonElementExtensionPropertyRoundTrip()
{
string json = @"{""MyIntMissing"":2, ""MyInt"":1}";
ClassWithIReadOnlyDictionaryExtensionPropertyAsJsonElementWithProperty obj = await Serializer.DeserializeWrapper<ClassWithIReadOnlyDictionaryExtensionPropertyAsJsonElementWithProperty>(json);

Assert.NotNull(obj.MyOverflow);
Assert.Equal(1, obj.MyInt);
Assert.Equal(2, obj.MyOverflow["MyIntMissing"].GetInt32());

string jsonSerialized = await Serializer.SerializeWrapper(obj);
Assert.Contains("\"MyIntMissing\"", jsonSerialized);
Assert.Contains("\"MyInt\"", jsonSerialized);
Assert.DoesNotContain(nameof(ClassWithIReadOnlyDictionaryExtensionPropertyAsJsonElementWithProperty.MyOverflow), jsonSerialized);
}

[Fact]
public async Task IReadOnlyDictionary_ExtensionPropertyIgnoredWhenWritingDefault()
{
string expected = @"{}";
string actual = await Serializer.SerializeWrapper(new ClassWithIReadOnlyDictionaryExtensionPropertyAsObject());
Assert.Equal(expected, actual);
}

public class ClassWithIReadOnlyDictionaryExtensionPropertyAsObject
{
[JsonExtensionData]
public IReadOnlyDictionary<string, object> MyOverflow { get; set; }
}

public class ClassWithIReadOnlyDictionaryExtensionPropertyAsJsonElement
{
[JsonExtensionData]
public IReadOnlyDictionary<string, JsonElement> MyOverflow { get; set; }
}

public class ClassWithIReadOnlyDictionaryExtensionPropertyAsObjectWithProperty
{
public int MyInt { get; set; }

[JsonExtensionData]
public IReadOnlyDictionary<string, object> MyOverflow { get; set; }
}

public class ClassWithIReadOnlyDictionaryExtensionPropertyAsJsonElementWithProperty
{
public int MyInt { get; set; }

[JsonExtensionData]
public IReadOnlyDictionary<string, JsonElement> MyOverflow { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,8 @@ public static void ClassWithExtensionDataAttribute_RemovingExtensionDataProperty
[Theory]
[InlineData(typeof(IDictionary<string, object>))]
[InlineData(typeof(IDictionary<string, JsonElement>))]
[InlineData(typeof(IReadOnlyDictionary<string, object>))]
[InlineData(typeof(IReadOnlyDictionary<string, JsonElement>))]
[InlineData(typeof(Dictionary<string, object>))]
[InlineData(typeof(Dictionary<string, JsonElement>))]
[InlineData(typeof(ConcurrentDictionary<string, JsonElement>))]
Expand Down
Loading