diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs index 2b774abc..a61d683b 100644 --- a/YamlDotNet.Test/Serialization/DeserializerTest.cs +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -24,6 +24,7 @@ using System.Linq; using FluentAssertions; using Xunit; +using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -216,7 +217,6 @@ public void NewLinesInKeys() Assert.Equal($"value\na\nb", dictionary.First().Value); } - [Theory] [InlineData(".nan", System.Single.NaN)] [InlineData(".NaN", System.Single.NaN)] @@ -283,6 +283,41 @@ public void DeserializeScalarEdgeCases(IConvertible value, Type type) result.Should().Be(value); } + [Fact] + public void DeserializeWithDuplicateKeyChecking_YamlWithDuplicateKeys_ThrowsYamlException() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +name: Jake +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithDuplicateKeyChecking() + .Build(); + + Action act = () => sut.Deserialize(yaml); + act.ShouldThrow("Because there are duplicate name keys"); + } + + [Fact] + public void DeserializeWithoutDuplicateKeyChecking_YamlWithDuplicateKeys_DoesNotThrowYamlException() + { + var yaml = @" +name: Jack +momentOfBirth: 1983-04-21T20:21:03.0041599Z +name: Jake +"; + + var sut = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + Action act = () => sut.Deserialize(yaml); + act.ShouldNotThrow("Because duplicate key checking is not enabled"); + } + public class Test { public string Value { get; set; } diff --git a/YamlDotNet/Serialization/DeserializerBuilder.cs b/YamlDotNet/Serialization/DeserializerBuilder.cs index 237eb3ca..89c11d59 100755 --- a/YamlDotNet/Serialization/DeserializerBuilder.cs +++ b/YamlDotNet/Serialization/DeserializerBuilder.cs @@ -47,6 +47,7 @@ public sealed class DeserializerBuilder : BuilderSkeleton private readonly Dictionary tagMappings; private readonly Dictionary typeMappings; private bool ignoreUnmatched; + private bool duplicateKeyChecking; private bool attemptUnknownTypeDeserialization; /// @@ -85,7 +86,7 @@ public DeserializerBuilder() { typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value) }, { typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value) }, { typeof(EnumerableNodeDeserializer), _ => new EnumerableNodeDeserializer() }, - { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, BuildTypeInspector(), ignoreUnmatched) } + { typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, BuildTypeInspector(), ignoreUnmatched, duplicateKeyChecking) } }; nodeTypeResolverFactories = new LazyComponentRegistrationList @@ -388,6 +389,16 @@ public DeserializerBuilder IgnoreUnmatchedProperties() return this; } + /// + /// Instructs the deserializer to check for duplicate keys and throw an exception if duplicate keys are found. + /// + /// + public DeserializerBuilder WithDuplicateKeyChecking() + { + duplicateKeyChecking = true; + return this; + } + /// /// Creates a new according to the current configuration. /// diff --git a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs index f7b40475..6938427c 100644 --- a/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs +++ b/YamlDotNet/Serialization/NodeDeserializers/ObjectNodeDeserializer.cs @@ -20,6 +20,7 @@ // SOFTWARE. using System; +using System.Collections.Generic; using System.Runtime.Serialization; using YamlDotNet.Core; using YamlDotNet.Core.Events; @@ -32,12 +33,14 @@ public sealed class ObjectNodeDeserializer : INodeDeserializer private readonly IObjectFactory objectFactory; private readonly ITypeInspector typeDescriptor; private readonly bool ignoreUnmatched; + private readonly bool duplicateKeyChecking; - public ObjectNodeDeserializer(IObjectFactory objectFactory, ITypeInspector typeDescriptor, bool ignoreUnmatched) + public ObjectNodeDeserializer(IObjectFactory objectFactory, ITypeInspector typeDescriptor, bool ignoreUnmatched, bool duplicateKeyChecking) { this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory)); this.typeDescriptor = typeDescriptor ?? throw new ArgumentNullException(nameof(typeDescriptor)); this.ignoreUnmatched = ignoreUnmatched; + this.duplicateKeyChecking = duplicateKeyChecking; } bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func nestedObjectDeserializer, out object? value) @@ -52,11 +55,17 @@ bool INodeDeserializer.Deserialize(IParser parser, Type expectedType, Func(); while (!parser.TryConsume(out var _)) { var propertyName = parser.Consume(); + if (duplicateKeyChecking && consumedProperties.Contains(propertyName.Value)) + { + throw new YamlException(propertyName.Start, propertyName.End, $"Encountered duplicate key {propertyName.Value}"); + } try { + consumedProperties.Add(propertyName.Value); var property = typeDescriptor.GetProperty(implementationType, null, propertyName.Value, ignoreUnmatched); if (property == null) {