Quick answer: IL2CPP’s managed code stripper removes any type, field, or method it cannot statically prove is used. Reflection-driven code (JsonUtility, custom serializers, dependency injection, plugin loaders) defeats static analysis, so the stripper deletes types that runtime code needs. Annotate affected types with [Preserve], add a link.xml file naming third-party assemblies you cannot modify, and inspect Editor.log for the linker report after every build to catch regressions.

Here is how to fix Unity IL2CPP when stripping removes types you actually need. Your game runs perfectly in the editor and in a Mono development build. You switch the scripting backend to IL2CPP for your release build, the build succeeds, and the game crashes the first time it tries to deserialize a save file. Or maybe it does not crash — it just silently returns a fresh object with default values, as if the JSON were empty. Or a plugin you load by reflection throws Type not found. The IL2CPP stripper is doing exactly what it is designed to do: shrink your binary by removing code it believes is dead.

The Symptom

Stripping bugs all share a common shape — the editor works, the IL2CPP build does not — but the symptoms vary by what the stripper removed:

JsonUtility returns defaults. You call JsonUtility.FromJson<SaveData>(json) in a build and get a SaveData with every field at its default value. The JSON is valid, the type still exists, but the fields you expected to populate are gone. The stripper deleted the backing fields because no static caller assigns them.

Type not found at runtime. You call Type.GetType("MyNamespace.MyClass") and get null. The class exists in your code but the stripper removed the type metadata because no static reference exists outside the reflection call.

Method invocation throws MissingMethodException. You invoke a method via MethodInfo.Invoke and get an exception saying the method does not exist. The class survived stripping but a specific method on it was deleted because no caller invoked it directly.

Generic instantiation fails. You call Activator.CreateInstance(typeof(Container<MyType>)) and get a runtime error about a missing type. The closed generic type was not generated because no static code instantiated it.

What Causes This

Static analysis cannot see reflection. The IL2CPP stripper builds a call graph by walking every method body and tracing references. It treats [RuntimeInitializeOnLoadMethod], scene-referenced MonoBehaviours, and a handful of well-known framework hooks as roots. Anything reachable from those roots survives. Reflection calls (Type.GetType, Activator.CreateInstance, JsonUtility.FromJson) reference types via strings or generic parameters that the analyzer cannot follow, so the targets look unused and get stripped.

Managed Stripping Level too aggressive. The Player Settings Managed Stripping Level dropdown ranges from Disabled through Minimal, Low, Medium, High. High uses Unity’s most aggressive ILLink rules and is most likely to cut reflection targets. Mobile defaults to Medium or higher to fit memory budgets.

JsonUtility specifically. JsonUtility uses reflection over fields, not properties. The stripper sees a field that is never read by any explicit getter and removes it. The field exists in your source but not in the binary, and JsonUtility has nothing to write into.

Closed generic instantiations. IL2CPP must AOT-compile every generic instantiation that the runtime uses. If the only path that creates Container<MyType> is reflection, the generic specialization is not emitted and instantiation fails at runtime.

The Fix

Step 1: Inspect Editor.log after the build. Open the editor log (Help > Open Editor Log on macOS, the standard Unity log on Windows). Search for Mono dependencies included in the build for the high-level summary, and for UnityLinker for the per-assembly stripping report. The linker writes a file you can grep to confirm a type survived. Missing types are missing from this report.

Step 2: Add [Preserve] to user-owned types. For any type your code defines that is touched only via reflection, add the attribute:

using System;
using UnityEngine;
using UnityEngine.Scripting;

// Preserve the whole class, including all fields, so JsonUtility
// has something to deserialize into in an IL2CPP build.
[Preserve]
[Serializable]
public class SaveData
{
    public int level;
    public float health;
    public string playerName;

    // Constructor must also be preserved or Activator fails.
    [Preserve]
    public SaveData() { }

    [Preserve]
    public SaveData(int lvl, float hp, string name)
    {
        level = lvl;
        health = hp;
        playerName = name;
    }
}

[Preserve] on the class keeps the type metadata and all fields. [Preserve] on a constructor or method keeps that specific member. The attribute is a hard signal to the stripper to ignore static analysis for the marked symbol.

Step 3: For third-party assemblies, write link.xml. If the type lives in a DLL you cannot edit (a NuGet package, a precompiled plugin, a Unity package whose source you do not own), create Assets/link.xml:

<!-- Assets/link.xml — preserves third-party reflection targets -->
<linker>
  <assembly fullname="Newtonsoft.Json">
    <type fullname="Newtonsoft.Json.JsonConvert" preserve="all" />
    <type fullname="Newtonsoft.Json.Serialization.DefaultContractResolver"
          preserve="all" />
  </assembly>

  <assembly fullname="MyGame.Plugins">
    <!-- Preserve every type whose namespace starts with Plugins.* -->
    <namespace fullname="MyGame.Plugins" preserve="all" />
  </assembly>

  <assembly fullname="Unity.Mathematics">
    <!-- Closed generics need explicit preservation -->
    <type fullname="Unity.Mathematics.float3" preserve="all" />
  </assembly>
</linker>

Multiple link.xml files anywhere in Assets are merged automatically, so you can scope per-package files alongside the package they cover. The preserve attribute accepts all, fields, methods, or nothing (the latter useful when you want to keep metadata but not the implementation).

Step 4: Lower Managed Stripping Level only as a last resort. If you cannot enumerate every reflection target and your binary size budget allows it, lower Managed Stripping Level from High to Medium or Low. Expect a build size increase of 10–30% on mobile, which can be the difference between fitting under the 200 MB cellular download cap on iOS and not.

Catching Stripping Bugs Earlier

The cycle of “build to device, test, find a stripping bug, edit link.xml, rebuild” is slow. Speed it up by enabling Strip Engine Code and the editor’s Use IL2CPP mode for your editor or by building a development player on desktop with the same Managed Stripping Level your release uses. Most stripping bugs reproduce on a desktop IL2CPP development build, and the iteration loop there is minutes instead of an hour for a mobile build.

Add a smoke test that exercises every reflection-driven path on application start in a development build, log success or failure for each, and gate your build pipeline on a clean run. The first save load, the first plugin load, the first JSON parse — if any of those fails on a development IL2CPP build, you know your link.xml is incomplete before the build hits a real device.

Generic Instantiation Hints

For closed generic types referenced only via reflection, also include a static reference in your code so IL2CPP emits the specialization. A trivial typeof(Container<MyType>) in a method that is reachable from a root is enough to force the specialization to ship, even if no other code calls it.

“The IL2CPP stripper is a perfect static analyzer of a language with reflection — which is to say, it cannot win. The fix is always to tell it explicitly what reflection will touch.”

Related Issues

If your IL2CPP build crashes during conversion rather than at runtime, see IL2CPP Build Errors. For iOS-specific IL2CPP crashes on launch, check IL2CPP Build Crash on iOS.

Annotate every reflection target with [Preserve], maintain one link.xml per third-party DLL, and grep Editor.log for the linker report on every build.