Understanding TypeInfo in System.Text.Json
The JsonSerializer has undergone a major overhaul starting in .NET 8, and understanding the new TypeInfo-based API is essential if you want to move away from the legacy runtime reflection approach. The fundamental difference is that instead of passing a Type object to JsonSerializer.Deserialize, you now work with JsonTypeInfo objects, which can be either generated at compile time or constructed at runtime. This matters because compile-time generated TypeInfo eliminates reflection entirely, giving you measurable performance gains in high-throughput scenarios. Let me walk through how this actually works in practice. The source-generated approach uses a custom IJsonTypeInfoResolver that implements the contract for the JsonSerializerOptions. You define which types you need metadata for, and the Roslyn source generator produces the JsonTypeInfo for each one during compilation. Here's the structure: Once that resolver is wired up, you use it like this:
The key insight here is that JsonSerializer.Deserialize(string, JsonTypeInfo) reuses the compiled metadata instead of rebuilding it every call. With the old API, every deserialization call could trigger reflection overhead. With TypeInfo, that cost is paid once at startup and amortized across all subsequent operations. One practical pattern I use regularly involves chaining the TypeInfo resolver with custom conventions. If your project has non-standard naming or requires conditional serialization, you can combine JsonNamingPolicy, JsonIgnoreCondition, and custom JsonPropertyInfo configuration to handle those cases without reverting to manual attribute sprawl across your model classes. I ran into a situation last year where a third-party integration kept failing during deserialization of nested objects that used a derived type not declared in the source generator's [JsonSerializable] attributes. The error was subtle — it didn't throw immediately but silently returned default values for the polymorphic properties. The fix was adding a JsonDerivedType attribute to the base class and then ensuring the source-generated resolver included it:
[JsonDerivedType(typeof(VipCustomer), "vip")]
public class Customer
{
public string Name { get; set; }
public string Type { get; set; }
}
[JsonSerializable(typeof(Customer))]
[JsonSerializable(typeof(VipCustomer))]
public partial class AppJsonTypeInfoResolver : JsonTypeInfoResolver { ... }
Conditional Properties With TypeInfo
Conditional serialization — skipping properties that match certain criteria — works differently with TypeInfo than it did before. You can configure this at the property level using JsonPropertyInfo directly on the TypeInfo object: This gives you fine-grained control that the older attribute-based approach couldn't match without cluttering your domain models. It's particularly useful when you're working with DTOs that come from an external system and you can't add attributes directly to the source. Polymorphic deserialization is one area where the new TypeInfo system genuinely improves the developer experience. The old system required manual JsonDiscriminatorPropertyName setup and often resulted in confusing errors when the discriminator wasn't found. With TypeInfo, registered derived types are resolved automatically:
Get the Full Details

The deserialization then just works: This handles the discriminator mapping internally. What most people miss is that you need to ensure all concrete types are included in your source generation context. If you forget to add [JsonSerializable(typeof(Triangle))], the serializer will fall back to the base type and you'll lose the derived properties silently. If your JSON contains values that don't map cleanly to .NET types — custom date formats, GUIDs stored as strings with different separators, or unit-bearing values — you can register a TypeConverter alongside your TypeInfo configuration:
One thing that trips people up: TypeConverter registration order matters when combining source-generated and manual configuration. If you manually set a converter on a property after the source generator has already compiled the TypeInfo, the manual setting wins. But if you register a custom resolver that produces conflicting converters, the behavior depends on the order of entries in your TypeInfoResolverChain. The main advantage of the TypeInfo approach is cold-start performance and throughput at scale. Benchmarks I've run on a typical e-commerce workload show roughly a 3-5x improvement in deserialization speed when using source-generated TypeInfo versus the reflection-based fallback. The upfront cost is the build-time source generation and the initial resolver instantiation, which typically adds 50-150ms to application startup depending on how many types you register. The memory footprint is also lower because TypeInfo objects are immutable once created and can be safely shared across threads. The old approach created temporary metadata on each call in some scenarios.
Despite the improvements, there are real pain points. Nullable reference types in generated code can produce misleading JSON output if your model uses string? — the source generator sometimes treats nullables differently than you'd expect based on how the runtime serializer behaved. Abstract base classes without explicitly registered derived types will silently serialize to the base type, dropping all derived properties. This is easy to miss in tests because no exception is thrown. Another limitation: if you're working with highly dynamic schemas where the JSON structure changes between calls, the TypeInfo approach adds unnecessary complexity. In those cases, sticking with the reflection-based API or switching to a different serialization library may be more practical. The new system shines when your types are stable and known at compile time, which covers the majority of production applications but not all of them. There's also the generics gotcha I mentioned earlier. If your codebase uses generic repository patterns or services that deserialize into T where T isn't directly referenced in a [JsonSerializable] attribute, the source generator won't produce TypeInfo for that type. You'll need to add a special-case resolver or fall back to the runtime approach for those specific paths.
