From 7a5d48267cf5e57a652e9f17a82115273f9557f8 Mon Sep 17 00:00:00 2001 From: Fastre <61952761+IamFastre@users.noreply.github.com> Date: Wed, 27 May 2026 05:47:29 +0300 Subject: [PATCH 1/2] Add support for uninitialized instance creation during deserialization, mainly to address positional properties records. * Added `UnstableAttemptUninitializedInstance` setting and related method `EnableUnstableAttemptUninitializedInstance` in `BuilderSkeleton`. * Now if `DefaultObjectFactory` fails while the option is on it tries `RuntimeHelpers`/`RuntimeSerialization`. * Added proper tests with positional properties records. --- .../Serialization/DeserializerTest.cs | 89 ++++++++++++++++++- YamlDotNet/Serialization/BuilderSkeleton.cs | 10 +++ .../ObjectFactories/DefaultObjectFactory.cs | 10 +++ YamlDotNet/Serialization/Settings.cs | 5 ++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs index 1f15322a0..36053209f 100644 --- a/YamlDotNet.Test/Serialization/DeserializerTest.cs +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -242,7 +242,7 @@ public void UnquotedStringTypeDeserialization_RegularNumbers(object expected) var yaml = FormattableString.Invariant($"Value: {expected}"); - // It needs explicitly specifying maximum precision for value roundtrip. + // It needs explicitly specifying maximum precision for value roundtrip. if (expected is float floatValue) { yaml = $"Value: {floatValue.ToString("G9", CultureInfo.InvariantCulture)}"; @@ -696,6 +696,93 @@ public enum EnumMemberedEnum } #endif +#if NET6_0_OR_GREATER + [Fact] + public void RecordClassWithPositionalProperties() + { + var deserializer = new DeserializerBuilder() + .EnableUnstableAttemptUninitializedInstance() + .Build(); + var yaml = @" +Name: Jack +Age: 43"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("Jack", actual.Name); + Assert.Equal(43, actual.Age); + } + + [Fact] + public void RecordStructWithPositionalProperties() + { + var deserializer = new DeserializerBuilder() + .EnableUnstableAttemptUninitializedInstance() + .Build(); + var yaml = @" +Name: Jack +Age: 43"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("Jack", actual.Name); + Assert.Equal(43, actual.Age); + } + + [Fact] + public void RecordClassWithPositionalPropertiesWithNamingConvention() + { + var deserializer = new DeserializerBuilder() + .EnableUnstableAttemptUninitializedInstance() + .WithNamingConvention(LowerCaseNamingConvention.Instance) + .Build(); + var yaml = @" +name: Jack +age: 43"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("Jack", actual.Name); + Assert.Equal(43, actual.Age); + } + + [Fact] + public void RecordStructWithPositionalPropertiesWithNamingConvention() + { + var deserializer = new DeserializerBuilder() + .EnableUnstableAttemptUninitializedInstance() + .WithNamingConvention(LowerCaseNamingConvention.Instance) + .Build(); + var yaml = @" +name: Jack +age: 43"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("Jack", actual.Name); + Assert.Equal(43, actual.Age); + } + + [Fact] + public void NestedRecordClassWithPositionalPropertiesWithNamingConvention() + { + var deserializer = new DeserializerBuilder() + .EnableUnstableAttemptUninitializedInstance() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); + var yaml = @" +name: Jack +year_of_birth: 1983 +car: + name: Mercedes + year: 2018"; + var actual = deserializer.Deserialize(yaml); + Assert.Equal("Jack", actual.Name); + Assert.Equal(1983, actual.YearOfBirth); + Assert.Equal("Mercedes", actual.Car.Name); + Assert.Equal(2018, actual.Car.Year); + } + + public record class PersonRecordClass(string Name, int Age); + public record struct PersonRecordStruct(string Name, int Age); + + public record PersonWithCardRecord(string Name, int YearOfBirth, CarRecord Car); + public record CarRecord(string Name, int Year); + +#endif + #nullable enable public class NullableDefaultClass { diff --git a/YamlDotNet/Serialization/BuilderSkeleton.cs b/YamlDotNet/Serialization/BuilderSkeleton.cs index 591d61ae8..729c0a75a 100755 --- a/YamlDotNet/Serialization/BuilderSkeleton.cs +++ b/YamlDotNet/Serialization/BuilderSkeleton.cs @@ -95,6 +95,16 @@ public TBuilder EnablePrivateConstructors() return Self; } + /// + /// Calling this will attempt to create an uninitialized instance during deserialization, possibly allowing certain types. + /// + /// This setting is UNSTABLE. + public TBuilder EnableUnstableAttemptUninitializedInstance() + { + settings.UnstableAttemptUninitializedInstance = true; + return Self; + } + /// /// Sets the that will be used by the (de)serializer. /// diff --git a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs index 7ded332ba..553f3d2af 100644 --- a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs +++ b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs @@ -25,7 +25,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using RuntimeSerialization = System.Runtime.Serialization; using YamlDotNet.Serialization.Callbacks; +using System.Runtime.CompilerServices; namespace YamlDotNet.Serialization.ObjectFactories { @@ -109,6 +111,14 @@ public override object Create(Type type) { return Activator.CreateInstance(type, settings.AllowPrivateConstructors)!; } + catch (MissingMethodException) when (settings.UnstableAttemptUninitializedInstance && !type.IsAbstract) + { +#if NET5_0_OR_GREATER + return RuntimeHelpers.GetUninitializedObject(type); +#else + return RuntimeSerialization.FormatterServices.GetUninitializedObject(type); +#endif + } catch (Exception err) { var message = $"Failed to create an instance of type '{type.FullName}'."; diff --git a/YamlDotNet/Serialization/Settings.cs b/YamlDotNet/Serialization/Settings.cs index 9eee575ac..4d2d94f25 100644 --- a/YamlDotNet/Serialization/Settings.cs +++ b/YamlDotNet/Serialization/Settings.cs @@ -27,5 +27,10 @@ public class Settings /// If true then private, parameterless constructors will be invoked if a public one is not available. /// public bool AllowPrivateConstructors { get; set; } + + /// + /// If true then constructors with positional properties will attempt to invoke if a public one is not available. + /// + public bool UnstableAttemptUninitializedInstance { get; set; } } } From f17e032f1fe2605a096b115f8cdb6c8fae4693af Mon Sep 17 00:00:00 2001 From: Fastre <61952761+IamFastre@users.noreply.github.com> Date: Thu, 28 May 2026 20:44:37 +0300 Subject: [PATCH 2/2] Some touchups and fixes relating to `UninitializedInstance` --- YamlDotNet.Test/Serialization/DeserializerTest.cs | 5 ++--- .../Serialization/ObjectFactories/DefaultObjectFactory.cs | 4 ++-- YamlDotNet/Serialization/Settings.cs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/YamlDotNet.Test/Serialization/DeserializerTest.cs b/YamlDotNet.Test/Serialization/DeserializerTest.cs index 36053209f..26d0bfff8 100644 --- a/YamlDotNet.Test/Serialization/DeserializerTest.cs +++ b/YamlDotNet.Test/Serialization/DeserializerTest.cs @@ -768,7 +768,7 @@ public void NestedRecordClassWithPositionalPropertiesWithNamingConvention() car: name: Mercedes year: 2018"; - var actual = deserializer.Deserialize(yaml); + var actual = deserializer.Deserialize(yaml); Assert.Equal("Jack", actual.Name); Assert.Equal(1983, actual.YearOfBirth); Assert.Equal("Mercedes", actual.Car.Name); @@ -778,9 +778,8 @@ public void NestedRecordClassWithPositionalPropertiesWithNamingConvention() public record class PersonRecordClass(string Name, int Age); public record struct PersonRecordStruct(string Name, int Age); - public record PersonWithCardRecord(string Name, int YearOfBirth, CarRecord Car); + public record PersonRecordWithCarRecord(string Name, int YearOfBirth, CarRecord Car); public record CarRecord(string Name, int Year); - #endif #nullable enable diff --git a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs index 553f3d2af..405ea7bd8 100644 --- a/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs +++ b/YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs @@ -25,9 +25,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using RuntimeSerialization = System.Runtime.Serialization; using YamlDotNet.Serialization.Callbacks; -using System.Runtime.CompilerServices; namespace YamlDotNet.Serialization.ObjectFactories { @@ -111,7 +111,7 @@ public override object Create(Type type) { return Activator.CreateInstance(type, settings.AllowPrivateConstructors)!; } - catch (MissingMethodException) when (settings.UnstableAttemptUninitializedInstance && !type.IsAbstract) + catch (MissingMethodException) when (settings.UnstableAttemptUninitializedInstance) { #if NET5_0_OR_GREATER return RuntimeHelpers.GetUninitializedObject(type); diff --git a/YamlDotNet/Serialization/Settings.cs b/YamlDotNet/Serialization/Settings.cs index 4d2d94f25..ac50b3af9 100644 --- a/YamlDotNet/Serialization/Settings.cs +++ b/YamlDotNet/Serialization/Settings.cs @@ -29,7 +29,7 @@ public class Settings public bool AllowPrivateConstructors { get; set; } /// - /// If true then constructors with positional properties will attempt to invoke if a public one is not available. + /// If true then will attempt to create an uninitialized instance during deserialization, possibly allowing certain type, such as records with positional properties. /// public bool UnstableAttemptUninitializedInstance { get; set; } }