Skip to content

Improve Native AOT Support (Closes #1085)#1092

Merged
EdwardCooke merged 11 commits intoaaubry:masterfrom
fdcastel:issue-1085
Apr 9, 2026
Merged

Improve Native AOT Support (Closes #1085)#1092
EdwardCooke merged 11 commits intoaaubry:masterfrom
fdcastel:issue-1085

Conversation

@fdcastel
Copy link
Copy Markdown
Contributor

@fdcastel fdcastel commented Mar 26, 2026

Based on #1089. Closes #1085.

This PR implements four of the seven suggestions from #1085 to improve YamlDotNet's Native AOT support via StaticDeserializerBuilder and the YamlDotNet.Analyzers.StaticGenerator source generator.

Changes

1. Better Error Messages for Unregistered Types (Suggestion #5)

Files changed: StaticObjectFactoryFile.cs, ObjectTests.cs

Replaced ArgumentOutOfRangeException with InvalidOperationException in the generated StaticObjectFactory code. The new error message clearly says:

Type 'MyNamespace.MyType' is not registered in the YamlDotNet static context. Add [YamlSerializable(typeof(MyType))] to your static context class.

This replaces the ambiguous "Unknown type: ..." message thrown as ArgumentOutOfRangeException, which was misleading and hard to diagnose.

2. Support required Members in Generated Object Factories (Suggestion #2)

Files changed: StaticObjectFactoryFile.cs, ObjectTests.cs

The source generator now detects C# required properties and fields (C# 11+) and emits object initializer syntax with default! values instead of bare new T(). This prevents CS9035 compile errors when types have required members.

Before (fails):

if (type == typeof(MyType)) return new MyType();

After (works):

if (type == typeof(MyType)) return new MyType() { Name = default!, Value = default! };

3. Support OrderedDictionary<TKey, TValue> in the Static Context (Suggestion #3)

Files changed: SerializableSyntaxReceiver.cs

Added recognition of System.Collections.Generic.OrderedDictionary<TKey, TValue> (.NET 9+) in the source generator's CheckForSupportedGeneric method. The type is now correctly identified as a dictionary, enabling proper code generation for IsDictionary, GetKeyType, GetValueType, and Create methods.

4. Built-in Type Converters for TimeSpan and Uri (Suggestion #4)

Files changed: TimeSpanConverter.cs (new), UriConverter.cs (new), BuilderSkeleton.cs, StaticBuilderSkeleton.cs, SerializerBuilder.cs, StaticSerializerBuilder.cs

Added TimeSpanConverter and UriConverter as built-in IYamlTypeConverter implementations, registered by default in both the regular and static builder skeletons. Both converters support JSON-compatible mode (double-quoted output).

Skipped Suggestions

The following suggestions were intentionally deferred:

  • 1. Publish Source Generator NuGet Package: Packaging/release concern, not a code change.
  • 6. Roslyn Analyzer for Missing Type Registrations: High-effort analyzer work requiring separate design.
  • 7. Document StaticDeserializerBuilder API Parity Gaps: Documentation-only task.

Testing

  • All existing tests pass (28 static generator tests on net8.0, 27 on net6.0).
  • New tests added:
    • UnregisteredTypeThrowsDescriptiveException — verifies the improved error message
    • RequiredMembersWork — verifies required property round-trip (net8.0 only)
    • TimeSpanConverterTests (9 tests) — unit + round-trip tests
    • UriConverterTests (8 tests) — unit + round-trip tests
  • Full solution builds successfully across all target frameworks (net8.0, net6.0, net47, netstandard2.0, netstandard2.1).

Pre-existing Test Failures

4 tests in UnquotedStringTypeDeserialization_RegularNumbers fail due to floating-point precision/locale issues unrelated to this PR. These failures also occur on master.

Add net9.0 and net10.0 to both the library and test project TargetFrameworks.

Fix OrderedDictionary ambiguity for net9.0+: .NET 9 introduced
System.Collections.Generic.OrderedDictionary<TKey, TValue> which conflicts
with YamlDotNet.Helpers.OrderedDictionary<TKey, TValue>. Disambiguate by
using fully qualified type names in YamlMappingNode.cs and
OrderedDictionaryTests.cs.

Update appveyor.yml to install the .NET 10 SDK and add artifact paths for
the new target frameworks.
@fdcastel fdcastel marked this pull request as draft March 26, 2026 20:51
@fdcastel
Copy link
Copy Markdown
Contributor Author

Added: Nullable type support for all built-in type converters

The latest commit (f6eba0e) extends all built-in converters to handle nullable types.

Problem

The converters introduced in this PR (TimeSpanConverter, UriConverter) and other pre-existing ones (GuidConverter, DateTimeConverter, DateTime8601Converter, DateTimeOffsetConverter, DateOnlyConverter, TimeOnlyConverter) only handled the base type — not the nullable counterpart. For example, TimeSpanConverter.Accepts(typeof(TimeSpan?)) returned false, so TimeSpan? properties were not handled by the converter at all.

Changes

All value-type converters (Guid, TimeSpan, DateTime, DateTimeOffset, DateOnly, TimeOnly):

Reference-type converter (Uri):

  • ReadYaml() and WriteYaml() now handle null values (same pattern — Accepts() didn't need changes since Uri is already nullable by nature)

SystemTypeConverter is intentionally excluded — already fixed by PR #1091.

Tests

Added NullableTypeConverterTests.cs with 18 tests covering:

  • Accepts() returns true for each Nullable<T> type
  • Round-trip (serialize → deserialize) for nullable properties with a value
  • Round-trip for nullable properties with null

All existing tests continue to pass.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Mar 28, 2026

Updated PR — New features added

This PR is now rebased on top of feature/add-net9-net10-targets (#1089) to take advantage of the .NET 9/10 target framework support.

Summary of changes

1. Improve error messages for unregistered types in static context

Clear, actionable error messages when a type is missing from the static context, instead of cryptic ArgumentOutOfRangeException.

2. Support required members in generated object factories

The source generator now correctly handles C# 11 required properties/fields in [YamlSerializable] types.

3. Support OrderedDictionary<TKey, TValue> in the static context

The static generator recognizes OrderedDictionary<TKey, TValue> (introduced in .NET 9) as a dictionary type, generating correct factory, accessor, and type inspector code.

4. Built-in type converters for TimeSpan and Uri

New TimeSpanConverter and UriConverter that handle serialization/deserialization with proper formatting control, similar to the existing DateTimeConverter.

5. Handle nullable types in all built-in type converters

All built-in converters (DateTimeConverter, DateTimeOffsetConverter, GuidConverter, TimeSpanConverter, UriConverter, DateOnlyConverter, TimeOnlyConverter) now correctly accept and unwrap Nullable<T> during deserialization instead of throwing.

6. IParsable<T> support in ScalarNodeDeserializer

Types with a static Parse(string, IFormatProvider) method — including all IParsable<T> implementations (.NET 7+) — are now automatically deserialized from YAML scalars without needing a custom IYamlTypeConverter. This is especially important for the static/AOT deserialization path where the NullTypeConverter cannot perform type conversions.

7. Roslyn diagnostic YDNG001 for unregistered types

The source generator now emits a compile-time warning when a property or field on a [YamlSerializable] type references a type that is not registered in the static context. The diagnostic checks types recursively, handles nullable types, skips well-known BCL primitives, and walks generic collection type arguments.

Test results

All tests pass on net8.0, net9.0, and net10.0 (1890 passing; the 4 locale-dependent UnquotedStringTypeDeserialization_RegularNumbers failures are pre-existing on master).

@fdcastel fdcastel marked this pull request as ready for review March 28, 2026 01:34
Replace ArgumentOutOfRangeException with InvalidOperationException in the
generated StaticObjectFactory code. The new error message clearly indicates
that the type is not registered and suggests adding [YamlSerializable] to
the static context class.

Addresses suggestion aaubry#5 from YAML_DOTNET_SUGGESTIONS.md.
The source generator now detects C# 'required' properties and fields and
emits object initializer syntax with 'default!' values instead of bare
'new T()'. This prevents CS9035 compile errors when types have required
members.

Generated code example:
  new MyType() { RequiredProp = default!, RequiredField = default! }

Addresses suggestion aaubry#2 from YAML_DOTNET_SUGGESTIONS.md.
Add recognition of System.Collections.Generic.OrderedDictionary<TKey, TValue>
(.NET 9+) in the source generator's CheckForSupportedGeneric method. The type
is now correctly identified as a dictionary, enabling proper code generation
for IsDictionary, GetKeyType, GetValueType, and Create methods.

Addresses suggestion aaubry#3 from YAML_DOTNET_SUGGESTIONS.md.
Add TimeSpanConverter and UriConverter as built-in IYamlTypeConverter
implementations, registered by default in both the regular and static
builder skeletons. This means TimeSpan and Uri values now round-trip
correctly without requiring users to write custom converters.

Both converters support JSON-compatible mode (double-quoted output) and
are automatically swapped in when JsonCompatible() is called on the
serializer builder.

Addresses suggestion aaubry#4 from YAML_DOTNET_SUGGESTIONS.md.
All value-type converters (Guid, TimeSpan, DateTime, DateTimeOffset,
DateOnly, TimeOnly) now accept their Nullable<T> counterpart.

All converters (including reference-type Uri) now handle null values
in ReadYaml (return null for empty/null scalars) and WriteYaml (emit
an empty scalar instead of throwing NullReferenceException).

SystemTypeConverter is intentionally excluded — it is already fixed
by PR aaubry#1091.
Types with a static Parse(string, IFormatProvider) method — such as
those implementing IParsable<T> (.NET 7+) — are now automatically
deserialized from YAML scalars without needing a custom IYamlTypeConverter.

This is especially important for the static/AOT deserialization path
where the NullTypeConverter cannot perform type conversions. Types like
TimeSpan, DateTimeOffset, DateOnly, TimeOnly, Guid, and IPAddress now
work out of the box in both the regular and static deserializer paths.
The source generator now emits a compile-time warning (YDNG001) when a
property or field on a [YamlSerializable] type references a type that
is not itself registered in the YamlDotNet static context.

This catches missing [YamlSerializable(typeof(T))] registrations at
compile time instead of at runtime, preventing ArgumentOutOfRangeException
errors during deserialization.

The diagnostic checks property/field types recursively, handles nullable
types, and skips well-known BCL types (primitives, Guid, TimeSpan, etc.)
and generic collections (verifying their element types instead).
@fdcastel fdcastel marked this pull request as draft March 28, 2026 01:51
@fdcastel fdcastel marked this pull request as ready for review March 28, 2026 02:00
@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Mar 28, 2026

I’m keeping all the changes in this PR for now, but feel free to suggest any modifications or ask me to split them into smaller PRs.

@fdcastel fdcastel changed the title Initial suggestions from #1085 Improving Native AOT Support (Closes #1085) Mar 28, 2026
@fdcastel fdcastel changed the title Improving Native AOT Support (Closes #1085) Improve Native AOT Support (Closes #1085) Mar 28, 2026
@EdwardCooke
Copy link
Copy Markdown
Collaborator

I merged in your other branches and now this one has conflicts. If you can get to them quickly I can merge them in in a few hours. Otherwise I'll probably create a branch from this PR and fix them in my repository and close this PR

@EdwardCooke
Copy link
Copy Markdown
Collaborator

Oh yeah, if you can remove net9.0 and net6.0 since those frameworks are EOL now that would be great.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 9, 2026

I merged in your other branches and now this one has conflicts. If you can get to them quickly I can merge them in in a few hours. Otherwise I'll probably create a branch from this PR and fix them in my repository and close this PR

Working on it... 👍🏻

# Conflicts:
#	YamlDotNet/Serialization/Converters/DateOnlyConverter.cs
#	YamlDotNet/Serialization/Converters/DateTime8601Converter.cs
#	YamlDotNet/Serialization/Converters/DateTimeConverter.cs
#	YamlDotNet/Serialization/Converters/DateTimeOffsetConverter.cs
#	YamlDotNet/Serialization/Converters/TimeOnlyConverter.cs
@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 9, 2026

Merged latest master into this branch to resolve conflicts.

Conflict Summary

5 files had conflicts, all in YamlDotNet/Serialization/Converters/ — caused by the ScalarConverterBase<T> extraction from PR #1090 overlapping with the nullable type handling added in this PR.

Conflicting files:

  • DateOnlyConverter.cs
  • DateTime8601Converter.cs
  • DateTimeConverter.cs
  • DateTimeOffsetConverter.cs
  • TimeOnlyConverter.cs

Resolution

Instead of keeping per-converter Accepts() overrides for nullable support, I moved the nullable type check into ScalarConverterBase<T>.Accepts():

public bool Accepts(Type type)
{
    return type == typeof(T) || Nullable.GetUnderlyingType(type) == typeof(T);
}

This gives all converters inheriting from ScalarConverterBase<T> automatic nullable type support without method hiding.

For ReadYaml and WriteYaml, the resolution combines both sides:

  • Uses ConsumeScalarValue(parser) helper from master's refactoring
  • Keeps the null/empty value checks needed for nullable type support
  • Keeps the null guard in WriteYaml (emitting empty scalar for null values)

Verification

  • Build succeeds across all 7 target frameworks (0 warnings, 0 errors)
  • All 201 converter tests pass
  • All 31 static generator tests pass

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 9, 2026

Oh yeah, if you can remove net9.0 and net6.0 since those frameworks are EOL now that would be great.

Doing...

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 9, 2026

Done — removed net9.0 and net6.0 from all projects (commit 45f94da).

Files updated:

  • YamlDotNet/YamlDotNet.csproj: net10.0;net8.0;netstandard2.0;netstandard2.1;net47
  • YamlDotNet.Test/YamlDotNet.Test.csproj: net10.0;net8.0;net47
  • YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj: net8.0;net47
  • appveyor.yml: removed Release-Net60 and Release-Net90 artifact entries
  • Dockerfile / Dockerfile.NonEnglish: removed net6.0 build and test steps

All 1972 tests pass on net8.0.

This was referenced Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Suggestions for Improving Native AOT Support

2 participants