diff --git a/YamlDotNet.Samples/ValidatingDuringDeserialization.cs b/YamlDotNet.Samples/ValidatingDuringDeserialization.cs index 0d55fb3bf..5e7f50ab5 100644 --- a/YamlDotNet.Samples/ValidatingDuringDeserialization.cs +++ b/YamlDotNet.Samples/ValidatingDuringDeserialization.cs @@ -43,9 +43,9 @@ public ValidatingNodeDeserializer(INodeDeserializer nodeDeserializer) this.nodeDeserializer = nodeDeserializer; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object value, object result) { - if (nodeDeserializer.Deserialize(parser, expectedType, nestedObjectDeserializer, out value)) + if (nodeDeserializer.Deserialize(parser, expectedType, nestedObjectDeserializer, out value, result)) { var context = new ValidationContext(value, null, null); Validator.ValidateObject(value, context, true); diff --git a/YamlDotNet.Test/Serialization/PopulateObjectTests.cs b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs new file mode 100644 index 000000000..59d08f566 --- /dev/null +++ b/YamlDotNet.Test/Serialization/PopulateObjectTests.cs @@ -0,0 +1,908 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Serialization; + +namespace YamlDotNet.Test.Serialization +{ + public class PopulateObjectTests : SerializationTestHelper + { + #region Simple objects + + class SimpleClass + { + public int Int { get; set; } + public string String { get; set; } + } + + class SimpleParentClass + { + public int Int { get; set; } + public string String { get; set; } + public SimpleClass Child { get; set; } + } + + [Fact] + public void PopulateSimpleObject() + { + var target = new SimpleClass { Int = 1, String = "one" }; + + var result = Deserializer.PopulateObject(@"Int: 2", target); + + Assert.Same(result, target); + + result.Int.Should().Be(2); + result.String.Should().Be("one"); + } + + [Fact] + public void PopulateSimpleObjectGraph() + { + var child = new SimpleClass { Int = 1, String = "one" }; + var target = new SimpleParentClass() + { + Int = 10, + String = "ten", + Child = child + }; + + var yaml = @" +Int: 20 +Child: + String: two"; + + var result = Deserializer.PopulateObject(yaml, target); + + result.Int.Should().Be(20); + + Assert.Same(result.Child, child); + + result.Child.Int.Should().Be(1); + result.Child.String.Should().Be("two"); + } + #endregion + + #region Simple structs + struct SimpleStruct + { + public int Int { get; set; } + public string String { get; set; } + } + + [Fact] + public void PopulateSimpleStruct() + { + var target = new SimpleStruct { Int = 1, String = "one" }; + + var result = Deserializer.PopulateObject(@"Int: 2", target); + + result.Int.Should().Be(2); + result.String.Should().Be("one"); + } + #endregion + + #region Populate collections + #region Populate collections: Reference types + [Fact] + public void PopulateArray_OfReferenceType_DeserializeSameSize() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, array); + + Assert.Same(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + } + + [Fact] + public void PopulateArray_OfReferenceType_DeserializeSmallerSize() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, array); + + Assert.Same(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("two"); + } + + [Fact] + public void PopulateArray_OfReferenceType_DeserializeBiggerSize() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +- + Int: 30 + String: THREE +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, array); + + Assert.NotSame(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(3); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("two"); + + result[2].Int.Should().Be(30); + result[2].String.Should().Be("THREE"); + } + + [Fact] + public void PopulateArray_OfReferenceType_DeserializeBiggerSize_PopulateItemsAllowGrowingArray() + { + var array = new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }; + + var firstItem = array[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +- + Int: 30 + String: THREE +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.PopulateItemsAllowGrowingArray) + .Build() + .PopulateObject(yaml, array); + + Assert.NotSame(result, array); + Assert.Same(result[0], firstItem); + + result.Length.Should().Be(3); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + + result[2].Int.Should().Be(30); + result[2].String.Should().Be("THREE"); + } + + [Fact] + public void PopulateList_OfReferenceType_DeserializeBiggerSize_PopulateOrAddItems() + { + var list = new List(new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }); + + var firstItem = list[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +- + Int: 30 + String: THREE +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.PopulateOrAddItems) + .Build() + .PopulateObject(yaml, list); + + Assert.Same(result, list); + Assert.Same(result[0], firstItem); + + result.Count.Should().Be(3); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + + result[2].Int.Should().Be(30); + result[2].String.Should().Be("THREE"); + } + + [Fact] + public void PopulateList_OfReferenceType_DeserializeEqualSize_PopulateOrAddItems() + { + var list = new List(new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }); + + var firstItem = list[0]; + + var yaml = @" +- + Int: 10 +- + String: TWO +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.PopulateOrAddItems) + .Build() + .PopulateObject(yaml, list); + + Assert.Same(result, list); + Assert.Same(result[0], firstItem); + + result.Count.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("TWO"); + } + + [Fact] + public void PopulateList_OfReferenceType_DeserializeSmallerSize_PopulateOrAddItems() + { + var list = new List(new[] + { + new SimpleClass { Int = 1, String = "one" }, + new SimpleClass { Int = 2, String = "two" } + }); + + var firstItem = list[0]; + + var yaml = @" +- + Int: 10 +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.PopulateOrAddItems) + .Build() + .PopulateObject(yaml, list); + + Assert.Same(result, list); + Assert.Same(result[0], firstItem); + + result.Count.Should().Be(2); + + result[0].Int.Should().Be(10); + result[0].String.Should().Be("one"); + + result[1].Int.Should().Be(2); + result[1].String.Should().Be("two"); + } + /* + [Fact] + public void PopulateReferenceTypeCollections() + { + var listOfDicts = new Dictionary>() + { + { "A", new Dictionary() + { + { "A1", "1" }, + { "A2", "2" } + } + } + }; + + var a = listOfDicts["A"]; + + var yaml = @" +A: + A1: 10 + C1: 30 +B: + B1: 100 +"; + + var result = DeserializerBuilder + .WithPopulatingOptions(ArrayPopulatingStrategy.PopulateItems) + .Build() + .PopulateObject(yaml, listOfDicts); + + Assert.Same(result, listOfDicts); + Assert.Same(result["A"], a); + result["A"].Count.Should().Be(3); + result["A"]["A1"].Should().Be("10"); + result.Count.Should().Be(2); + result["B"]["B1"].Should().Be("100"); + } + */ + #endregion + + #region Populate collections: Arrays of value types + class IntArrayContainer + { + public int[] Ints { get; set; } + } + + [Fact] + public void PopulateArray_OfValueType_AsNestedNode_CreateNew() + { + var intArray = new int[] { 1, 2 }; + var container = new IntArrayContainer { Ints = intArray }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.CreateNew) + .Build() + .PopulateObject(yaml, container); + + // original array should remain unchanged + Assert.NotSame(result.Ints, intArray); + intArray.ShouldBeEquivalentTo(new[] { 1, 2 }); + + result.Ints.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateArray_OfValueType_AsNestedNode_FillExisting() + { + var intArray = new int[] { 1, 2 }; + var container = new IntArrayContainer { Ints = intArray }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.FillExisting) + .Build() + .PopulateObject(yaml, container); + + Assert.Same(result.Ints, intArray); + + result.Ints.ShouldBeEquivalentTo(new[] { 10, 2 }); + } + + [Fact] + public void PopulateArray_OfValueType_AsNestedNode_FillExisting_WithInsufficientSize() + { + var intArray = new int[] { 1, 2 }; + var container = new IntArrayContainer { Ints = intArray }; + + var yaml = @" +Ints: +- 10 +- 20 +- 30"; + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.FillExisting) + .Build() + .PopulateObject(yaml, container); + + action.ShouldThrow().Where(ex => ex.Message.Contains("exceed size")); + } + + [Fact] + public void PopulateArray_OfValueType_AsRootNode_CreateNew() + { + var intArray = new int[] { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.CreateNew) + .Build() + .PopulateObject(yaml, intArray); + + // original array should remain unchanged + Assert.NotSame(result, intArray); + intArray.ShouldAllBeEquivalentTo(new[] { 1, 2 }); + + result.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateArray_OfValueType_AsRootNode_FillExisting() + { + var intArray = new int[] { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(arrayStrategy: ArrayPopulatingStrategy.FillExisting) + .Build() + .PopulateObject(yaml, intArray); + + Assert.Same(result, intArray); + + result.ShouldBeEquivalentTo(new[] { 10, 2 }); + } + #endregion + + #region Populate collections: Collections (lists) of value types + public class IntGenericCollectionContainer + { + public IList Ints { get; set; } + } + + [Fact] + public void PopulateCollection_OfValueType_AsNestedNode_CreateNew() + { + var intCollection = new List { 1, 2 }; + var container = new IntGenericCollectionContainer { Ints = intCollection }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.CreateNew) + .Build() + .PopulateObject(yaml, container); + + Assert.NotSame(result.Ints, intCollection); + result.Ints.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateCollection_OfValueType_AsNestedNode_AddItems() + { + var intCollection = new List { 1, 2 }; + var container = new IntGenericCollectionContainer { Ints = intCollection }; + + var yaml = @" +Ints: +- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.AddItems) + .Build() + .PopulateObject(yaml, container); + + Assert.Same(result.Ints, intCollection); + result.Ints.ShouldBeEquivalentTo(new[] { 1, 2, 10 }); + } + + [Fact] + public void PopulateCollection_OfValueType_AsRootNode_CreateNew() + { + var intCollection = new List { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.CreateNew) + .Build() + .PopulateObject(yaml, intCollection); + + Assert.NotSame(result, intCollection); + result.ShouldBeEquivalentTo(new[] { 10 }); + } + + [Fact] + public void PopulateCollection_OfValueType_AsRootNode_AddItems() + { + var intCollection = new List { 1, 2 }; + + var yaml = @"- 10"; + + var result = DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.AddItems) + .Build() + .PopulateObject(yaml, intCollection); + + Assert.Same(result, intCollection); + result.ShouldBeEquivalentTo(new[] { 1, 2, 10 }); + } + #endregion + + #region Populate collections: Dictionaries of value types + class StringIntDictionaryContainer + { + public Dictionary StringInts { get; set; } + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsNestedNode_CreateNew() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + var container = new StringIntDictionaryContainer { StringInts = stringInts }; + + var yaml = @" +StringInts: + one: 10 + three: 30"; + + var result = DeserializerBuilder + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.CreateNew) + .Build() + .PopulateObject(yaml, container); + + Assert.NotSame(result.StringInts, stringInts); + container.StringInts.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "three", 30 } + }); + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsNestedNode_AddItemsReplaceExistingKeys() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + var container = new StringIntDictionaryContainer { StringInts = stringInts }; + + var yaml = @" +StringInts: + one: 10 + three: 30"; + + var result = DeserializerBuilder + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsReplaceExistingKeys) + .Build() + .PopulateObject(yaml, container); + + Assert.Same(result.StringInts, stringInts); + container.StringInts.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "two", 2 }, + { "three", 30 } + }); + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsNestedNode_AddItemsThrowOnExistingKeys() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + var container = new StringIntDictionaryContainer { StringInts = stringInts }; + + var yaml = @" +StringInts: + one: 10 + three: 30"; + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsThrowOnExistingKeys) + .Build() + .PopulateObject(yaml, container); + + action.ShouldThrow().WithInnerException().Where(ex => ex.InnerException.Message.Contains("same key")); + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsRootNode_CreateNew() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + + var yaml = @" +one: 10 +three: 30"; + + var result = DeserializerBuilder + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.CreateNew) + .Build() + .PopulateObject(yaml, stringInts); + + Assert.NotSame(result, stringInts); + result.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "three", 30 } + }); + } + + [Fact] + public void PopulateDictionary_OfValueTypes_AsRootNode_AddItemsReplaceExistingKeys() + { + var stringInts = new Dictionary + { + { "one", 1 }, + { "two", 2 }, + }; + + var yaml = @" +one: 10 +three: 30"; + + var result = DeserializerBuilder + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsReplaceExistingKeys) + .Build() + .PopulateObject(yaml, stringInts); + + Assert.Same(result, stringInts); + result.ShouldBeEquivalentTo(new Dictionary { + { "one", 10 }, + { "two", 2 }, + { "three", 30 } + }); + } + #endregion + + #region Edge cases + class GenericListButNotNonGenericList : IList + { + public T this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public int Count => throw new NotImplementedException(); + + public bool IsReadOnly => throw new NotImplementedException(); + + public void Add(T item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(T item) + { + throw new NotImplementedException(); + } + + public void CopyTo(T[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + public int IndexOf(T item) + { + throw new NotImplementedException(); + } + + public void Insert(int index, T item) + { + throw new NotImplementedException(); + } + + public bool Remove(T item) + { + throw new NotImplementedException(); + } + + public void RemoveAt(int index) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + [Fact] + public void PopulateList_WithTypeNotSupportedForPopulating_CreateNew() + { + var list = new GenericListButNotNonGenericList(); + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.CreateNew) + .Build() + .PopulateObject("- one", list); + + action.ShouldThrow().WithInnerException(); + } + + [Fact] + public void PopulateList_WithTypeNotSupportedForPopulating_AddItems() + { + var list = new GenericListButNotNonGenericList(); + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(collectionStrategy: CollectionPopulatingStrategy.AddItems) + .Build() + .PopulateObject("- one", list); + + action.ShouldThrow().WithInnerException(); + } + + class GenericDictionaryButNotNonGenericDictionary : IDictionary + { + public TValue this[TKey key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public ICollection Keys => throw new NotImplementedException(); + + public ICollection Values => throw new NotImplementedException(); + + public int Count => throw new NotImplementedException(); + + public bool IsReadOnly => throw new NotImplementedException(); + + public void Add(TKey key, TValue value) + { + throw new NotImplementedException(); + } + + public void Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool ContainsKey(TKey key) + { + throw new NotImplementedException(); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + public bool Remove(TKey key) + { + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + [Fact] + public void PopulateDictionary_WithTypeNotSupportedForPopulating_CreateNew() + { + var dictionary = new GenericDictionaryButNotNonGenericDictionary(); + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.CreateNew) + .Build() + .PopulateObject("one: ten", dictionary); + + action.ShouldThrow().WithInnerException(); + } + + [Fact] + public void PopulateDictionary_WithTypeNotSupportedForPopulating_AddItemsReplaceExistingKeys() + { + var dictionary = new GenericDictionaryButNotNonGenericDictionary(); + + Action action = () => DeserializerBuilder + .WithPopulatingOptions(dictionaryStrategy: DictionaryPopulatingStrategy.AddItemsReplaceExistingKeys) + .Build() + .PopulateObject("one: ten", dictionary); + + action.ShouldThrow().WithInnerException(); + } + #endregion + #endregion + } +} diff --git a/YamlDotNet/Serialization/Deserializer.cs b/YamlDotNet/Serialization/Deserializer.cs index 82acc4e3f..2ea2fab19 100644 --- a/YamlDotNet/Serialization/Deserializer.cs +++ b/YamlDotNet/Serialization/Deserializer.cs @@ -90,17 +90,17 @@ public T Deserialize(TextReader input) public object? Deserialize(TextReader input, Type type) { - return Deserialize(new Parser(input), type); + return Deserialize(new Parser(input), type, null); } public T Deserialize(IParser parser) { - return (T)Deserialize(parser, typeof(T))!; // We really want an exception if we are trying to deserialize null into a non-nullable type + return (T)Deserialize(parser, typeof(T), null)!; // We really want an exception if we are trying to deserialize null into a non-nullable type } public object? Deserialize(IParser parser) { - return Deserialize(parser, typeof(object)); + return Deserialize(parser, typeof(object), null); } /// @@ -110,6 +110,27 @@ public T Deserialize(IParser parser) /// The static type of the object to deserialize. /// Returns the deserialized object. public object? Deserialize(IParser parser, Type type) + { + return Deserialize(parser, type, null); + } + + public T PopulateObject(string input, T target) + { + using var reader = new StringReader(input); + return PopulateObject(reader, target); + } + + public T PopulateObject(TextReader input, T target) + { + return PopulateObject(new Parser(input), target); + } + + public T PopulateObject(IParser parser, T target) + { + return (T)Deserialize(parser, typeof(T), target)!; + } + + private object? Deserialize(IParser parser, Type type, object? target) { if (parser == null) { @@ -125,11 +146,11 @@ public T Deserialize(IParser parser) var hasDocumentStart = parser.TryConsume(out var _); - object? result = null; + object? result = target; if (!parser.Accept(out var _) && !parser.Accept(out var _)) { using var state = new SerializerState(); - result = valueDeserializer.DeserializeValue(parser, type, state, valueDeserializer); + result = valueDeserializer.DeserializeValue(parser, type, state, valueDeserializer, result); state.OnDeserialization(); } diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index 3ad041de6..59d67057b 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -20,6 +20,7 @@ // SOFTWARE. using System; +using System.Collections; using System.Collections.Generic; using YamlDotNet.Core; using YamlDotNet.Serialization.NamingConventions; @@ -363,6 +364,25 @@ public DeserializerBuilder IgnoreUnmatchedProperties() return this; } + /// + /// Configures how pre-existing collections are handled when using and overloads. + /// + /// + /// + /// + /// + public DeserializerBuilder WithPopulatingOptions( + ArrayPopulatingStrategy arrayStrategy = ArrayPopulatingStrategy.CreateNew, + CollectionPopulatingStrategy collectionStrategy = CollectionPopulatingStrategy.CreateNew, + DictionaryPopulatingStrategy dictionaryStrategy = DictionaryPopulatingStrategy.CreateNew) + { + nodeDeserializerFactories.Replace(typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer(arrayStrategy)); + nodeDeserializerFactories.Replace(typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value, dictionaryStrategy)); + nodeDeserializerFactories.Replace(typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value, collectionStrategy)); + + return this; + } + /// /// Creates a new according to the current configuration. /// diff --git a/YamlDotNet/Serialization/IDeserializer.cs b/YamlDotNet/Serialization/IDeserializer.cs index c41066a5b..d75db8ffa 100644 --- a/YamlDotNet/Serialization/IDeserializer.cs +++ b/YamlDotNet/Serialization/IDeserializer.cs @@ -42,5 +42,18 @@ public interface IDeserializer /// The static type of the object to deserialize. /// Returns the deserialized object. object? Deserialize(IParser parser, Type type); + + T PopulateObject(string input, T target); + T PopulateObject(TextReader input, T target); + + /// + /// Populates a pre-existing object. Values of fields/properties missing in the given YAML remain unchanged. + /// Use to configure how pre-existing collections are handled. + /// + /// The type of the target object. + /// The from where to deserialize the object. + /// The target object to be populated. + /// Returns the target object with values populated from the deserialized YAML. + T PopulateObject(IParser parser, T target); } } diff --git a/YamlDotNet/Serialization/INodeDeserializer.cs b/YamlDotNet/Serialization/INodeDeserializer.cs index ed3cb04f1..db32ee434 100644 --- a/YamlDotNet/Serialization/INodeDeserializer.cs +++ b/YamlDotNet/Serialization/INodeDeserializer.cs @@ -26,6 +26,6 @@ namespace YamlDotNet.Serialization { public interface INodeDeserializer { - bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value); + bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue); } } diff --git a/YamlDotNet/Serialization/IValueDeserializer.cs b/YamlDotNet/Serialization/IValueDeserializer.cs index 8ff5fe32b..6281143e2 100644 --- a/YamlDotNet/Serialization/IValueDeserializer.cs +++ b/YamlDotNet/Serialization/IValueDeserializer.cs @@ -27,6 +27,6 @@ namespace YamlDotNet.Serialization { public interface IValueDeserializer { - object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer); + object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer, object? result); } } diff --git a/YamlDotNet/Serialization/LazyComponentRegistrationList.cs b/YamlDotNet/Serialization/LazyComponentRegistrationList.cs index 4692af08b..83a725eba 100644 --- a/YamlDotNet/Serialization/LazyComponentRegistrationList.cs +++ b/YamlDotNet/Serialization/LazyComponentRegistrationList.cs @@ -70,13 +70,24 @@ public void Add(Type componentType, Func factory) } public void Remove(Type componentType) + { + var index = IndexOf(componentType); + entries.RemoveAt(index); + } + + public void Replace(Type componentType, Func factory) + { + var index = IndexOf(componentType); + entries[index] = new LazyComponentRegistration(componentType, factory); + } + + private int IndexOf(Type componentType) { for (var i = 0; i < entries.Count; ++i) { if (entries[i].ComponentType == componentType) { - entries.RemoveAt(i); - return; + return i; } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs index 1e62c17fb..5eabf6ea2 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ArrayNodeDeserializer.cs @@ -27,7 +27,14 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class ArrayNodeDeserializer : INodeDeserializer { - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + private readonly ArrayPopulatingStrategy populatingStrategy; + + public ArrayNodeDeserializer(ArrayPopulatingStrategy populatingStrategy = ArrayPopulatingStrategy.CreateNew) + { + this.populatingStrategy = populatingStrategy; + } + + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (!expectedType.IsArray) { @@ -37,16 +44,53 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { IList? list; var canUpdate = true; @@ -49,7 +51,8 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func)); canUpdate = genericListType != null; list = (IList?)Activator.CreateInstance(typeof(GenericCollectionToNonGenericAdapter<>).MakeGenericType(itemType), value); + + // TODO: How to handle pre-existing instance in this case? + if (populatingStrategy != CollectionPopulatingStrategy.CreateNew) + { + throw new NotSupportedException($"Types implementing generic interface {typeof(IList<>).Name} but not non-generic interface {typeof(IList).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populatingStrategy}."); + } } } else if (typeof(IList).IsAssignableFrom(expectedType)) { itemType = typeof(object); - value = objectFactory.Create(expectedType); + value = (currentValue == null || populatingStrategy == CollectionPopulatingStrategy.CreateNew) ? objectFactory.Create(expectedType) : currentValue; list = (IList)value; } else @@ -72,25 +81,32 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, IList result, bool canUpdate) + internal static void DeserializeHelper(Type tItem, IParser parser, Func nestedObjectDeserializer, IList result, bool canUpdate, CollectionPopulatingStrategy populatingStrategy = CollectionPopulatingStrategy.CreateNew) { parser.Consume(); + var i = 0; while (!parser.TryConsume(out var _)) { var current = parser.Current; - var value = nestedObjectDeserializer(parser, tItem); + var currentValue = populatingStrategy == CollectionPopulatingStrategy.PopulateOrAddItems ? (result.Count > i ? result[i] : null) : null; + var isPopulating = currentValue != null; + + var value = nestedObjectDeserializer(parser, tItem, currentValue); if (value is IValuePromise promise) { if (canUpdate) { - var index = result.Add(tItem.IsValueType() ? Activator.CreateInstance(tItem) : null); - promise.ValueAvailable += v => result[index] = TypeConverter.ChangeType(v, tItem); + if (isPopulating == false) + { + var index = result.Add(tItem.IsValueType() ? Activator.CreateInstance(tItem) : null); + promise.ValueAvailable += v => result[index] = TypeConverter.ChangeType(v, tItem); + } } else { @@ -103,8 +119,13 @@ internal static void DeserializeHelper(Type tItem, IParser parser, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { IDictionary? dictionary; Type keyType, valueType; @@ -49,13 +51,18 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func but not IDictionary dictionary = (IDictionary?)Activator.CreateInstance(typeof(GenericDictionaryToNonGenericAdapter<,>).MakeGenericType(keyType, valueType), value); + + // TODO: How to handle pre-existing instance in this case? + if (populatingStrategy != DictionaryPopulatingStrategy.CreateNew) + { + throw new NotSupportedException($"Types implementing generic interface {typeof(IDictionary<,>).Name} but not non-generic interface {typeof(IDictionary).Name} are not yet supported when using {nameof(Deserializer.PopulateObject)}() in combination with {populatingStrategy}."); + } } } else if (typeof(IDictionary).IsAssignableFrom(expectedType)) @@ -63,7 +70,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, IDictionary result) + private void DeserializeHelper(Type tKey, Type tValue, IParser parser, Func nestedObjectDeserializer, IDictionary result) { parser.Consume(); while (!parser.TryConsume(out var _)) { - var key = nestedObjectDeserializer(parser, tKey); - var value = nestedObjectDeserializer(parser, tValue); + var key = nestedObjectDeserializer(parser, tKey, null); + var value = nestedObjectDeserializer(parser, tValue, null); var valuePromise = value as IValuePromise; if (key is IValuePromise keyPromise) @@ -91,7 +98,7 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu if (valuePromise == null) { // Key is pending, value is known - keyPromise.ValueAvailable += v => result[v!] = value!; + keyPromise.ValueAvailable += v => AddKeyValuePair(result, v!, value!); ; } else { @@ -102,7 +109,7 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu { if (hasFirstPart) { - result[v!] = value!; + AddKeyValuePair(result, v!, value!); } else { @@ -115,7 +122,7 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu { if (hasFirstPart) { - result[key] = v!; + AddKeyValuePair(result, key!, v!); } else { @@ -130,15 +137,33 @@ private static void DeserializeHelper(Type tKey, Type tValue, IParser parser, Fu if (valuePromise == null) { // Happy path: both key and value are known - result[key!] = value!; + AddKeyValuePair(result, key!, value!); } else { // Key is known, value is pending - valuePromise.ValueAvailable += v => result[key!] = v!; + valuePromise.ValueAvailable += v => AddKeyValuePair(result, key!, v!); } } } } + + private void AddKeyValuePair(IDictionary result, object key, object value) + { + switch (populatingStrategy) + { + case DictionaryPopulatingStrategy.AddItemsThrowOnExistingKeys: + result.Add(key!, value!); + break; + + case DictionaryPopulatingStrategy.CreateNew: + case DictionaryPopulatingStrategy.AddItemsReplaceExistingKeys: + result[key!] = value!; + break; + + default: + throw new NotSupportedException(); + } + } } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs index aed879d8e..f324a4515 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/EnumerableNodeDeserializer.cs @@ -29,7 +29,7 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class EnumerableNodeDeserializer : INodeDeserializer { - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { Type itemsType; if (expectedType == typeof(IEnumerable)) @@ -49,7 +49,7 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func).MakeGenericType(itemsType); - value = nestedObjectDeserializer(parser, collectionType); + value = nestedObjectDeserializer(parser, collectionType, null); return true; } } diff --git a/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs index 7f334fa0b..0bd8aac04 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/NullNodeDeserializer.cs @@ -27,7 +27,7 @@ namespace YamlDotNet.Serialization.NodeDeserializers { public sealed class NullNodeDeserializer : INodeDeserializer { - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { value = null; if (parser.Accept(out var evt)) diff --git a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs index a401abf16..552c7122b 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs @@ -40,7 +40,7 @@ public ObjectNodeDeserializer(IObjectFactory objectFactory, ITypeInspector typeD this.ignoreUnmatched = ignoreUnmatched; } - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (!parser.TryConsume(out var mapping)) { @@ -51,7 +51,8 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func(out var _)) { var propertyName = parser.Consume(); @@ -64,7 +65,9 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (!parser.TryConsume(out var scalar)) { diff --git a/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs index d00420229..68f8c103e 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/TypeConverterNodeDeserializer.cs @@ -35,7 +35,7 @@ public TypeConverterNodeDeserializer(IEnumerable converters) this.converters = converters ?? throw new ArgumentNullException(nameof(converters)); } - bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { var converter = converters.FirstOrDefault(c => c.Accepts(expectedType)); if (converter == null) diff --git a/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs index 22fe9a26a..b9a83ac91 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/YamlConvertibleNodeDeserializer.cs @@ -33,12 +33,12 @@ public YamlConvertibleNodeDeserializer(IObjectFactory objectFactory) this.objectFactory = objectFactory; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { if (typeof(IYamlConvertible).IsAssignableFrom(expectedType)) { var convertible = (IYamlConvertible)objectFactory.Create(expectedType); - convertible.Read(parser, expectedType, type => nestedObjectDeserializer(parser, type)); + convertible.Read(parser, expectedType, type => nestedObjectDeserializer(parser, type, null)); value = convertible; return true; } diff --git a/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs index 44c17f66c..36715dd66 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/YamlSerializableNodeDeserializer.cs @@ -33,7 +33,7 @@ public YamlSerializableNodeDeserializer(IObjectFactory objectFactory) this.objectFactory = objectFactory; } - public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) + public bool Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value, object? currentValue) { #pragma warning disable 0618 // IYamlSerializable is obsolete if (typeof(IYamlSerializable).IsAssignableFrom(expectedType)) diff --git a/YamlDotNet/Serialization/PopulatingStrategies.cs b/YamlDotNet/Serialization/PopulatingStrategies.cs new file mode 100644 index 000000000..351ef30a3 --- /dev/null +++ b/YamlDotNet/Serialization/PopulatingStrategies.cs @@ -0,0 +1,90 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; +using YamlDotNet.Core; + +namespace YamlDotNet.Serialization +{ + /// + /// Defines how arrays will be treated during deserialization when populating a pre-existing object (via and overloads) + /// + public enum ArrayPopulatingStrategy + { + /// + /// Always create a new instance. + /// + CreateNew, + /// + /// Fill the pre-existing array, overwriting items. Pre-existing values that do not get overwritten will be kept. + /// If the pre-existing array does not have sufficient length, a will be thrown. + /// If the array does not exist yet, an instance will be created and populated. + /// + FillExisting, + PopulateItems, + PopulateItemsAllowGrowingArray + } + + /// + /// Defines how types inherting from and will be treated during deserialization when populating a pre-existing object (via and overloads) + /// + public enum CollectionPopulatingStrategy + { + /// + /// Always create a new instance. + /// + CreateNew, + /// + /// Add items to the pre-existing collection. + /// If the collection does not exist yet, an instance will be created and populated. + /// + AddItems, + /// + /// Populate pre-existing items, create and add new items where there is no pre-existing target. + /// If the collection does not exist yet, an instance will be created and populated. + /// + PopulateOrAddItems + } + + /// + /// Defines how types inherting from and will be treated during deserialization when populating a pre-existing object (via and overloads) + /// + public enum DictionaryPopulatingStrategy + { + /// + /// Always create a new instance. + /// + CreateNew, + /// + /// Add items to the pre-existing dictionary. + /// If the key is already present in the pre-existing collection, an will be thrown. + /// If the dictionary does not exist yet, an instance will be created and populated. + /// + AddItemsThrowOnExistingKeys, + /// + /// Add items to the pre-existing dictionary, replacing values with pre-existing keys. + /// If the dictionary does not exist yet, an instance will be created and populated. + /// + AddItemsReplaceExistingKeys + } +} diff --git a/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs b/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs index 3a50d129a..0b1f689d8 100644 --- a/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs +++ b/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs @@ -96,7 +96,7 @@ public object? Value } } - public object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer) + public object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer, object? currentValue) { object? value; if (parser.TryConsume(out var alias)) @@ -121,7 +121,7 @@ public object? Value } } - value = innerDeserializer.DeserializeValue(parser, expectedType, state, nestedObjectDeserializer); + value = innerDeserializer.DeserializeValue(parser, expectedType, state, nestedObjectDeserializer, currentValue); if (!anchor.IsEmpty) { diff --git a/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs b/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs index 5bb9b8d59..73e09b64c 100644 --- a/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs +++ b/YamlDotNet/Serialization/ValueDeserializers/NodeValueDeserializer.cs @@ -38,7 +38,7 @@ public NodeValueDeserializer(IList deserializers, IList(out var nodeEvent); var nodeType = GetTypeFromEvent(nodeEvent, expectedType); @@ -47,7 +47,7 @@ public NodeValueDeserializer(IList deserializers, IList nestedObjectDeserializer.DeserializeValue(r, t, state, nestedObjectDeserializer), out var value)) + if (deserializer.Deserialize(parser, nodeType, (r, t, o) => nestedObjectDeserializer.DeserializeValue(r, t, state, nestedObjectDeserializer, o), out var value, currentValue)) { return TypeConverter.ChangeType(value, expectedType); }