diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs index 2864dfa0cc8aa..cd837e2248cea 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/MetadataTests/DefaultJsonTypeInfoResolverTests.JsonTypeInfo.cs @@ -218,6 +218,22 @@ public static void TypeInfoKindNoneNumberHandling() Assert.Equal(testObj.IntProp, deserialized.IntProp); } + [Theory] + [InlineData(typeof(List), JsonTypeInfoKind.Enumerable)] + [InlineData(typeof(Dictionary), JsonTypeInfoKind.Dictionary)] + [InlineData(typeof(object), JsonTypeInfoKind.None)] + [InlineData(typeof(string), JsonTypeInfoKind.None)] + public static void AddingPropertyToNonObjectJsonTypeInfoKindThrows(Type type, JsonTypeInfoKind expectedKind) + { + JsonSerializerOptions options = new(); + DefaultJsonTypeInfoResolver resolver = new(); + JsonTypeInfo typeInfo = resolver.GetTypeInfo(type, options); + Assert.Equal(expectedKind, typeInfo.Kind); + + JsonPropertyInfo property = typeInfo.CreateJsonPropertyInfo(typeof(int), "test"); + Assert.Throws(() => typeInfo.Properties.Add(property)); + } + [Fact] public static void RecursiveTypeNumberHandling() { diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs index ebece0937fa46..3924e8f1e83bd 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/TypeInfoResolverFunctionalTests.cs @@ -250,7 +250,7 @@ public static void SetCustomNumberHandlingForAProperty() } [Fact] - public static void SetCustomConverterForAProperty() + public static void SetCustomConverterForIntProperty() { DefaultJsonTypeInfoResolver resolver = new(); resolver.Modifiers.Add((ti) => @@ -287,21 +287,21 @@ public static void SetCustomConverterForAProperty() } [Fact] - public static void UntypedCreateObjectWithDefaults() + public static void SetCustomConverterForListProperty() { DefaultJsonTypeInfoResolver resolver = new(); resolver.Modifiers.Add((ti) => { - if (ti.Type == typeof(TestClass)) + if (ti.Type == typeof(TestClassWithLists)) { - ti.CreateObject = () => + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) { - return new TestClass() + if (prop.Name == nameof(TestClassWithLists.ListProperty1)) { - TestField = "test value", - TestProperty = 42, - }; - }; + prop.CustomConverter = new AddListEntryConverter(); + } + } } }); @@ -309,48 +309,92 @@ public static void UntypedCreateObjectWithDefaults() options.IncludeFields = true; options.TypeInfoResolver = resolver; - TestClass originalObj = new TestClass() + TestClassWithLists originalObj = new TestClassWithLists() { - TestField = "test value 2", - TestProperty = 45, + ListProperty1 = new List { 2, 3 }, + ListProperty2 = new List { 4, 5, 6 }, }; string json = JsonSerializer.Serialize(originalObj, options); - Assert.Equal(@"{""TestProperty"":45,""TestField"":""test value 2""}", json); + Assert.Equal(@"{""ListProperty1"":[2,3,-1],""ListProperty2"":[4,5,6]}", json); - TestClass deserialized = JsonSerializer.Deserialize(json, options); - Assert.Equal(originalObj.TestField, deserialized.TestField); - Assert.Equal(originalObj.TestProperty, deserialized.TestProperty); + TestClassWithLists deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.ListProperty1, deserialized.ListProperty1); + Assert.Equal(originalObj.ListProperty2, deserialized.ListProperty2); + } - json = @"{}"; - deserialized = JsonSerializer.Deserialize(json, options); - Assert.Equal("test value", deserialized.TestField); - Assert.Equal(42, deserialized.TestProperty); + [Fact] + public static void SetCustomConverterForDictionaryProperty() + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(TestClassWithDictionaries)) + { + Assert.Equal(JsonTypeInfoKind.Object, ti.Kind); + foreach (var prop in ti.Properties) + { + if (prop.Name == nameof(TestClassWithDictionaries.DictionaryProperty1)) + { + prop.CustomConverter = new AddDictionaryEntryConverter(); + } + } + } + }); - json = @"{""TestField"":""test value 2""}"; - deserialized = JsonSerializer.Deserialize(json, options); - Assert.Equal(originalObj.TestField, deserialized.TestField); - Assert.Equal(42, deserialized.TestProperty); + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClassWithDictionaries originalObj = new TestClassWithDictionaries() + { + DictionaryProperty1 = new Dictionary + { + ["test1"] = 4, + ["test2"] = 5, + }, + DictionaryProperty2 = new Dictionary + { + ["foo"] = 1, + ["bar"] = 8, + }, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal("""{"DictionaryProperty1":{"test1":4,"test2":5,"*test*":-1},"DictionaryProperty2":{"foo":1,"bar":8}}""", json); + + TestClassWithDictionaries deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(originalObj.DictionaryProperty1, deserialized.DictionaryProperty1); + Assert.Equal(originalObj.DictionaryProperty2, deserialized.DictionaryProperty2); } - [Fact] - public static void TypedCreateObjectWithDefaults() + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void CreateObjectWithDefaults(bool useTypedCreateObject) { DefaultJsonTypeInfoResolver resolver = new(); resolver.Modifiers.Add((ti) => { if (ti.Type == typeof(TestClass)) { - JsonTypeInfo typedTi = ti as JsonTypeInfo; - Assert.NotNull(typedTi); - typedTi.CreateObject = () => + Func createObj = () => new TestClass() { - return new TestClass() - { - TestField = "test value", - TestProperty = 42, - }; + TestField = "test value", + TestProperty = 42, }; + + if (useTypedCreateObject) + { + JsonTypeInfo typedTi = ti as JsonTypeInfo; + Assert.NotNull(typedTi); + typedTi.CreateObject = createObj; + } + else + { + // we want to make sure Func is not a cast to the untyped one + ti.CreateObject = () => createObj(); + } } }); @@ -382,6 +426,114 @@ public static void TypedCreateObjectWithDefaults() Assert.Equal(42, deserialized.TestProperty); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void CreateObjectForListWithDefaults(bool useTypedCreateObject) + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(List)) + { + Func> createObj = () => new List { 99 }; + + if (useTypedCreateObject) + { + JsonTypeInfo> typedTi = ti as JsonTypeInfo>; + Assert.NotNull(typedTi); + typedTi.CreateObject = createObj; + } + else + { + // we want to make sure Func is not a cast to the untyped one + ti.CreateObject = () => createObj(); + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClassWithLists originalObj = new TestClassWithLists() + { + ListProperty1 = new List { 2, 3 }, + ListProperty2 = new List { }, + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal("""{"ListProperty1":[2,3],"ListProperty2":[]}""", json); + + TestClassWithLists deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(new List { 99, 2, 3 }, deserialized.ListProperty1); + Assert.Equal(new List { 99 }, deserialized.ListProperty2); + + json = @"{}"; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Null(deserialized.ListProperty1); + Assert.Null(deserialized.ListProperty2); + + json = """{"ListProperty2":[ 123 ]}"""; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Null(deserialized.ListProperty1); + Assert.Equal(new List { 99, 123 }, deserialized.ListProperty2); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void CreateObjectForDictionaryWithDefaults(bool useTypedCreateObject) + { + DefaultJsonTypeInfoResolver resolver = new(); + resolver.Modifiers.Add((ti) => + { + if (ti.Type == typeof(Dictionary)) + { + Func> createObj = () => new Dictionary { ["*test*"] = -1 }; + + if (useTypedCreateObject) + { + JsonTypeInfo> typedTi = ti as JsonTypeInfo>; + Assert.NotNull(typedTi); + typedTi.CreateObject = createObj; + } + else + { + // we want to make sure Func is not a cast to the untyped one + ti.CreateObject = () => createObj(); + } + } + }); + + JsonSerializerOptions options = new JsonSerializerOptions(); + options.IncludeFields = true; + options.TypeInfoResolver = resolver; + + TestClassWithDictionaries originalObj = new() + { + DictionaryProperty1 = new Dictionary { ["test1"] = 2, ["test2"] = 3 }, + DictionaryProperty2 = new Dictionary(), + }; + + string json = JsonSerializer.Serialize(originalObj, options); + Assert.Equal("""{"DictionaryProperty1":{"test1":2,"test2":3},"DictionaryProperty2":{}}""", json); + + TestClassWithDictionaries deserialized = JsonSerializer.Deserialize(json, options); + Assert.Equal(new Dictionary { ["*test*"] = -1, ["test1"] = 2, ["test2"] = 3 }, deserialized.DictionaryProperty1); + Assert.Equal(new Dictionary { ["*test*"] = -1 }, deserialized.DictionaryProperty2); + + json = @"{}"; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Null(deserialized.DictionaryProperty1); + Assert.Null(deserialized.DictionaryProperty2); + + json = """{"DictionaryProperty2":{"foo":123}}"""; + deserialized = JsonSerializer.Deserialize(json, options); + Assert.Null(deserialized.DictionaryProperty1); + Assert.Equal(new Dictionary { ["*test*"] = -1, ["foo"] = 123 }, deserialized.DictionaryProperty2); + } + [Fact] public static void SetCustomNumberHandlingForAType() { @@ -685,6 +837,18 @@ internal class TestClass public string TestField; } + internal class TestClassWithLists + { + public List ListProperty1 { get; set; } + public List ListProperty2 { get; set; } + } + + internal class TestClassWithDictionaries + { + public Dictionary DictionaryProperty1 { get; set; } + public Dictionary DictionaryProperty2 { get; set; } + } + // adds one on write, subtracts one on read internal class PlusOneConverter : JsonConverter { @@ -699,5 +863,112 @@ public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptio writer.WriteNumberValue(value + 1); } } + + // adds list entry in the end on write, removes one on read + internal class AddListEntryConverter : JsonConverter> + { + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Assert.Equal(typeof(List), typeToConvert); + Assert.Equal(JsonTokenType.StartArray, reader.TokenType); + + List list = new(); + int? lastEntry = null; + while (true) + { + Assert.True(reader.Read()); + + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + if (lastEntry.HasValue) + { + // note: we never add last entry + list.Add(lastEntry.Value); + } + + Assert.Equal(JsonTokenType.Number, reader.TokenType); + lastEntry = reader.GetInt32(); + } + + Assert.True(lastEntry.HasValue); + Assert.Equal(-1, lastEntry.Value); + + return list; + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (int element in value) + { + writer.WriteNumberValue(element); + } + + writer.WriteNumberValue(-1); + writer.WriteEndArray(); + } + } + + // Adds extra dictionary entry on write, removes it on read + internal class AddDictionaryEntryConverter : JsonConverter> + { + public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Assert.Equal(typeof(Dictionary), typeToConvert); + Assert.Equal(JsonTokenType.StartObject, reader.TokenType); + + Dictionary dict = new(); + KeyValuePair? lastEntry = null; + + while (true) + { + Assert.True(reader.Read()); + + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (lastEntry.HasValue) + { + // note: we never add last entry + dict.Add(lastEntry.Value.Key, lastEntry.Value.Value); + } + + Assert.Equal(JsonTokenType.PropertyName, reader.TokenType); + string? key = reader.GetString(); + Assert.NotNull(key); + Assert.True(reader.Read()); + + Assert.Equal(JsonTokenType.Number, reader.TokenType); + lastEntry = new KeyValuePair(key, reader.GetInt32()); + } + + Assert.True(lastEntry.HasValue); + Assert.Equal("*test*", lastEntry.Value.Key); + Assert.Equal(-1, lastEntry.Value.Value); + + return dict; + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + foreach (var kv in value) + { + writer.WritePropertyName(kv.Key); + writer.WriteNumberValue(kv.Value); + } + + writer.WritePropertyName("*test*"); + writer.WriteNumberValue(-1); + writer.WriteEndObject(); + } + } } }