Type.FullName returns null when the type is not generic type definition but contains generic paramters.

The rational behind this design is to ensure Type.FullName (if not null) can uniquely identify a type in an assembly; or given the string returned by Type.FullName, Type.GetType(string typeName) can return the original type back. It is hard to keep this invariant if
!type.IsGenericTypeDefinition && type.ContainsGenericParameters.

For instance, suppose we have an assembly compiled with the following C# code:

class G<T> {
    public void M<S>() { }
}

typeof(G<>).FullName is "G`1", and we can round trip this type from Type.GetType("G`1"). But we can build more complicated generic types, such as G<S> (type G<> bound with the generic parameter from the method M<>); in order to identify such type with a string, a lot of extra information is need.

Below are some examples where Type.FullName returns null.

class G<T> {
  public class C { }
  public void M(C arg) { }
}
class G2<T> : G<T> { }

string s1 = typeof(G<>).GetGenericArguments()[0].FullName;
// T in G<T>: generic parameter
string s2 = typeof(G<>).GetMethod("M").GetParameters()[0].ParameterType.FullName;
// check out the IL, it is G`1/C<!T> (not generic type definition) 
// Related topic, see this

string s3 = typeof(G2<>).BaseType.FullName;
// base type of G2<>, which is not generic type definition either
// it equals to typeof(G<>).MakeGenericType(typeof(G2<>).GetGenericArguments()[0])