Quick answer: NativeAOT trims unused code aggressively. Mark reflection targets with [DynamicDependency] or list types in an rd.xml runtime directives file.
A Godot C# game builds with NativeAOT for faster startup. Runtime throws “type was not preserved” when deserializing save data. AOT trimmed the type.
DynamicDependency Attribute
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SaveData))]
public void LoadGame()
{
var data = JsonSerializer.Deserialize<SaveData>(json);
}
Tells the trimmer: keep SaveData and all its members, this method needs them via reflection.
Runtime Directives (rd.xml)
<Directives>
<Application>
<Assembly Name="GameAssembly">
<Type Name="Game.SaveData" Dynamic="Required All" />
</Assembly>
</Application>
</Directives>
Reference in .csproj: <RdXmlFile Include="rd.xml" />.
Prefer Source Generators
System.Text.Json source generator avoids reflection entirely:
[JsonSerializable(typeof(SaveData))]
internal partial class GameJsonContext : JsonSerializerContext { }
// usage
JsonSerializer.Deserialize(json, GameJsonContext.Default.SaveData);
Compile-time generated; fully AOT-safe. Best long-term solution.
Test AOT Early
Don’t leave AOT testing until ship. Build NativeAOT in CI from day one — reflection issues surface as you add code, not in a giant pile at the end.
Verifying
Build NativeAOT export. Load/save works. No “type not preserved”. Startup time improvement realized.
“AOT trims what it can’t see. Mark reflection targets explicitly or, better, eliminate reflection with source generators.”
For console ports, NativeAOT is often required — start AOT-clean and you save a painful porting phase later.