Almost every .NET service spends real, measurable time turning objects into JSON and back, and almost every senior engineer has, at some point, chased a latency or memory problem straight back to the serializer. These questions go past "which library do you use" into how System.Text.Json actually behaves under load, where it diverges from Newtonsoft.Json in ways that break naive migrations, and how to keep it safe when the input comes from outside your trust boundary. Expect interviewers to push on specifics — default settings, attribute names, and what happens when a payload is malicious rather than merely malformed.

Q1 What are the real trade-offs between System.Text.Json and Newtonsoft.Json today, and would you ever still choose Newtonsoft.Json for a new high-throughput service?#

Short answer: System.Text.Json is faster and allocates less in the common case, ships in the box, is required for Native AOT, and is strict and security-conscious by default; Newtonsoft.Json is more permissive and has a broader feature set out of the box — JsonPath queries, TypeNameHandling.All for open-ended polymorphism, more exotic contract customization. For a new high-throughput service, the answer is almost always System.Text.Json, and the exceptions are narrow and specific.

The performance gap comes from design, not just tuning: System.Text.Json is built on Utf8JsonReader and Utf8JsonWriter, which operate directly on UTF-8 bytes and Span<byte>, avoiding the UTF-16 string allocations Newtonsoft.Json's JsonTextReader/JsonTextWriter incur when working through TextReader/ TextWriter. Source generation removes reflection-based metadata resolution entirely for known types. The behavioral gap matters just as much for a migration decision: System.Text.Json is case-sensitive by default where Newtonsoft.Json matches property names case-insensitively; it throws on comments and trailing commas unless you opt in, where Newtonsoft.Json tolerates both; and it deliberately does not support Newtonsoft.Json's TypeNameHandling.All, which lets a payload specify an arbitrary assembly-qualified type name to instantiate — a documented, historically exploited deserialization attack surface that System.Text.Json's polymorphism model avoids by requiring every valid derived type to be declared explicitly in code ahead of time. The reasons you'd still pick Newtonsoft.Json for something new are narrow: heavy reliance on JsonPath queries against arbitrary untyped documents, deep integration with a library that only supports Newtonsoft.Json contracts, or a genuinely small, low-throughput service where developer familiarity outweighs the performance delta.

AspectSystem.Text.JsonNewtonsoft.Json
Default property matchingCase-sensitiveCase-insensitive
Comments/trailing commasRejected by defaultAllowed by default
Open-ended $type polymorphismNot supported (by design)Supported via TypeNameHandling
JsonPath queriesNot supportedSupported via JToken.SelectToken
Native AOT / trimmingFully supported with source generationNot supported
Typical allocation profileLower (UTF-8, Span<T>-based)Higher (UTF-16 TextReader/Writer)

What interviewers look for: a decision framed around concrete, checkable differences rather than "STJ is just faster," and explicit awareness that some of the gap is a deliberate security trade-off, not an oversight.

  • Follow-up questions: why does System.Text.Json refuse to support TypeNameHandling.All-style polymorphism? What would you tell a team that wants to keep Newtonsoft.Json purely for its more forgiving defaults?

Q2 How does System.Text.Json source generation work, and what does it actually buy you over the reflection default?#

Short answer: Source generation produces a compile-time JsonSerializerContext that supplies type metadata the serializer would otherwise have to build via reflection at first use, which removes startup and steady-state reflection cost, is required (not just faster) under trimming and Native AOT, and can additionally generate specialized serialization code paths for maximum throughput.

You opt in by declaring a partial class deriving from JsonSerializerContext and marking each root type you serialize with [JsonSerializable]; the source generator then emits the metadata — and, depending on mode, the serialization logic itself — at compile time instead of the runtime reflecting over your types the first time they're serialized. There are two generation modes. Metadata-based mode (the default) generates fast, cached type metadata but still uses the general-purpose serialization engine, and supports every System.Text.Json feature, including asynchronous streaming. Serialization-optimization mode (the "fast path," set via JsonSourceGenerationOptionsAttribute.GenerationMode) additionally generates direct, specialized serialize methods per type, which is faster still for synchronous serialization of already-known types, but isn't used for asynchronous serialization — streaming async serialization always needs the metadata-based path, though System.Text.Json automatically falls back to it when payloads are small enough to fit a single buffer.

C#
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(OrderResponse))]
[JsonSerializable(typeof(List<OrderResponse>))]
internal partial class AppJsonContext : JsonSerializerContext;

// Use the generated context instead of the reflection-based overload.
string json = JsonSerializer.Serialize(order, AppJsonContext.Default.OrderResponse);
OrderResponse? parsed = JsonSerializer.Deserialize(json, AppJsonContext.Default.OrderResponse);

Beyond raw speed, source generation is what makes System.Text.Json compatible with trimming and Native AOT: reflection-based serialization needs metadata the trimmer can't statically prove is used, so it either gets trimmed away (breaking at runtime) or the trimmer has to keep far more than necessary. Most of the options you'd set on a JsonSerializerOptions instance at runtime — naming policy, indentation, number handling — can also be set on the JsonSourceGenerationOptionsAttribute at compile time, so the generated context's Default property comes preconfigured.

What interviewers look for: distinguishing metadata-based generation from the serialization-only fast path, and knowing that source generation isn't only a performance feature — it's a hard requirement, not an optimization, for Native AOT deployments.

  • Follow-up questions: why can't the fast-path mode be used for asynchronous streaming serialization? How would you combine source generation for your own types with reflection fallback for third-party types you don't control?

Q3 How do you implement polymorphic serialization in System.Text.Json, and what are the security implications of getting it wrong?#

Short answer: You explicitly opt a base type into polymorphism with [JsonPolymorphic] and declare every allowed derived type with [JsonDerivedType(typeof(X), "discriminator")]; the security-relevant point is that this is a closed, compile-time allowlist by design, unlike open-ended $type-driven deserialization, which is exactly the attack surface System.Text.Json avoids by construction.

At serialization time, the type discriminator — a string or integer you choose per derived type — is written into a property named $type by default (customizable via JsonPolymorphicAttribute.TypeDiscriminatorPropertyName), alongside the derived type's own members. At deserialization time, the serializer reads that discriminator and instantiates the matching declared type — and only a declared type; an unrecognized discriminator value doesn't get to instantiate an arbitrary class from your AppDomain, it's handled according to JsonUnknownDerivedTypeHandling, which can fail deserialization outright, fall back to the base type, or fall back to the nearest matching ancestor, all explicitly configured choices rather than an open-ended default.

C#
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$eventType")]
[JsonDerivedType(typeof(OrderPlaced), "orderPlaced")]
[JsonDerivedType(typeof(OrderCancelled), "orderCancelled")]
public abstract record DomainEvent(Guid OrderId, DateTimeOffset OccurredAt);

public sealed record OrderPlaced(Guid OrderId, DateTimeOffset OccurredAt, decimal Total)
    : DomainEvent(OrderId, OccurredAt);

public sealed record OrderCancelled(Guid OrderId, DateTimeOffset OccurredAt, string Reason)
    : DomainEvent(OrderId, OccurredAt);

The security implication is the contrast with what Newtonsoft.Json's TypeNameHandling.All allows: a payload can specify an assembly-qualified type name and the deserializer will attempt to construct that type, which becomes dangerous the moment an attacker controls the payload and the application exposes any type in its dependency graph with exploitable side effects in its constructor or property setters — a well-documented class of deserialization remote-code-execution vulnerability. System.Text.Json's model closes that off structurally: there is no way to deserialize into a type that wasn't explicitly declared with [JsonDerivedType] on that specific base type, so the "getting it wrong" failure mode isn't usually a security hole — it's a design mistake like using a broad object property instead of a declared polymorphic hierarchy, which then either loses type information on round-trip or requires a manual, easy-to-get-wrong JsonConverter<object>.

What interviewers look for: the security contrast with open-ended type-name-driven deserialization stated explicitly, and comfort with the actual attribute names and discriminator mechanics, not just "you can do polymorphism somehow."

  • Common mistakes: trying to deserialize into a base type typed as object and expecting the concrete derived type to come back automatically without any [JsonDerivedType] declarations at all.
  • Follow-up questions: how would you evolve a discriminated event contract over time without breaking consumers still on an older derived type list? What does IgnoreUnrecognizedTypeDiscriminators change about deserialization behavior?

Q4 How do you serialize and deserialize very large JSON payloads without loading the whole document into memory?#

Short answer: Stream directly against the underlying Stream rather than materializing a string or a full JsonDocument first — JsonSerializer.SerializeAsync/DeserializeAsync against a Stream for whole-object streaming, and JsonSerializer.DeserializeAsyncEnumerable<T> when the payload is a large JSON array you want to process one element at a time.

The naive path — read the whole response body into a string, then call JsonSerializer.Deserialize on that string — pays for two full copies of the data in memory (the string and the resulting object graph) plus the UTF-8-to-UTF-16 conversion cost of materializing the string in the first place, all before a single property is available. Serializing and deserializing directly against a Stream avoids the intermediate string entirely, working over UTF-8 bytes throughout. For a large array specifically — paging through millions of records, or consuming a large export — DeserializeAsyncEnumerable<T> yields each element as it becomes available, so peak memory is bounded by one element's footprint plus internal buffering rather than the size of the entire array.

C#
await using var responseStream = await httpClient.GetStreamAsync(exportUrl, ct);

await foreach (var record in JsonSerializer.DeserializeAsyncEnumerable<ExportRecord>(
    responseStream, JsonContext.Default.ExportRecord, ct))
{
    await ProcessRecordAsync(record, ct);
}

For cases where you only need a handful of fields out of a large or unpredictable document — and don't want to define a full POCO for a payload you don't fully control — Utf8JsonReader gives you a forward-only, allocation-minimal cursor over the raw UTF-8 bytes, letting you skip irrelevant sections without ever materializing them as objects. JsonDocument/JsonElement sit in between: they parse the whole document into a compact, UTF-8-backed DOM you can navigate without a fixed target type, which is lighter than full object deserialization but still holds the entire document in memory, unlike Utf8JsonReader or the async-enumerable streaming path.

What interviewers look for: knowing the specific API for streaming a large array (DeserializeAsyncEnumerable<T>) rather than a vague "stream it somehow," and the ability to explain concretely what the naive string-based path costs in memory and conversions.

  • Follow-up questions: how would backpressure work if the consumer processing each element in the await foreach loop is slower than the network can deliver data? When would Utf8JsonReader still beat DeserializeAsyncEnumerable<T>?

Q5 What security settings matter when deserializing JSON from an untrusted source, and what does MaxDepth actually protect against?#

Short answer: JsonSerializerOptions.MaxDepth bounds how deeply nested a JSON document is allowed to be before deserialization fails, defending against stack-overflow attacks from a maliciously deeply-nested payload; beyond depth, the other load-bearing defaults are the size limit enforced upstream by the host (Kestrel's MaxRequestBodySize), strict type resolution (no open-ended $type deserialization), and deliberate numeric and string handling that doesn't silently coerce unexpected input.

A recursive-descent JSON parser walks nested objects and arrays by recursion, and without a depth limit, a payload like thousands of nested [[[[[...]]]]]] brackets can drive the parser's call stack deep enough to crash the process with a StackOverflowException — which, unlike most .NET exceptions, cannot be caught, so it takes the whole process down. MaxDepth defaults to 64, matching Newtonsoft.Json's own default, and applies during both serialization and deserialization; a payload nested deeper than the configured limit throws a catchable JsonException well before the stack is at risk. That's one layer of a broader defense for any endpoint accepting JSON from outside your trust boundary: Kestrel's MaxRequestBodySize and MinRequestBodyDataRate (covered in high-throughput-services-interview) stop a request from consuming unbounded memory or holding a connection open indefinitely before the JSON layer ever sees the whole body; System.Text.Json's refusal to do open-ended, $type-driven polymorphic deserialization closes off the deserialization-RCE class of attack; and strict-by-default parsing — no silently tolerating comments, no case-insensitive property matching unless you opt in — reduces the chance that a malformed or adversarial payload gets interpreted differently than you expect. A team that deserializes untrusted payloads directly into types with side-effecting constructors or property setters is still exposed regardless of which JSON library it uses, so reviewing what actually happens when those setters run is as important as any serializer configuration.

What interviewers look for: naming MaxDepth and its default specifically, tying it to a concrete attack (stack exhaustion) rather than a vague "it's for security," and situating it as one layer among several — request size limits, strict type resolution, and safe target types — rather than the whole defense.

  • Common mistakes: believing JSON deserialization is inherently safe because "it's just data," and overlooking that request size and depth limits both need to be set deliberately for any endpoint open to external traffic.
  • Follow-up questions: how would you defend a legacy endpoint that still uses Newtonsoft.Json with TypeNameHandling enabled for backward compatibility? What's the risk of raising MaxDepth far above its default to accommodate one unusually nested legitimate payload shape?

Q6 Walk through writing a custom JsonConverter<T>. When do you need one instead of the built-in attributes?#

Short answer: A custom converter is for representations the built-in attribute model can't express — a type with no natural one-to-one property mapping, a third-party type you can't annotate, or a wire format that differs structurally from the CLR shape — implemented by overriding Read and Write against Utf8JsonReader/Utf8JsonWriter directly rather than describing property-level rules.

Reach for attributes ([JsonPropertyName], [JsonIgnore], [JsonConverter] on a single property, [JsonNumberHandling]) first, since they're simpler and composable; write a full JsonConverter<T> when the JSON shape doesn't map to the type's shape at all — serializing a value object as a single JSON string instead of an object with properties, parsing a third-party type you don't own and can't annotate, or handling a field whose JSON representation depends on runtime data the attribute model can't see.

C#
public sealed class MoneyConverter : JsonConverter<Money>
{
    public override Money Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var text = reader.GetString() ?? throw new JsonException("Expected a money string.");
        return Money.Parse(text); // e.g. "129.99 USD"
    }

    public override void Write(Utf8JsonWriter writer, Money value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }
}

// Registered once, globally:
options.Converters.Add(new MoneyConverter());

Register the converter globally on JsonSerializerOptions.Converters for a type used consistently across the app, or scope it to a single property with [JsonConverter(typeof(MoneyConverter))] when only that one property needs the special handling. A converter must be careful about Utf8JsonReader being a ref struct — it can't be captured in a closure, stored in a field, or used across an await, which pushes you toward fully consuming the token(s) you need synchronously within Read before returning. When source generation is in play, a type with a registered custom converter is still supported, but the generator defers to the converter for that type instead of generating specialized metadata for it.

What interviewers look for: a clear line between "this is an attribute-level concern" and "this needs a full converter," plus awareness of the ref struct constraint on Utf8JsonReader, which trips up anyone's first attempt at a converter that tries to store the reader for later use.

  • Follow-up questions: how would you write a converter for a type that needs access to a value from a sibling property to deserialize correctly? Why can't Utf8JsonReader be used inside an async method?

Q7 What are the most common gotchas when migrating an existing codebase from Newtonsoft.Json to System.Text.Json?#

Short answer: Almost every migration surprise comes from System.Text.Json being strict by default where Newtonsoft.Json was permissive: case-sensitive property matching, no comments or trailing commas without opt-in, no open-ended TypeNameHandling-based polymorphism, and no built-in JsonPath querying — each of these needs an explicit decision, not a like-for-like drop-in replacement.

The table-of-differences gotchas that bite real migrations most often: case sensitivity — a payload with "OrderId" won't bind to a property named OrderId if the client actually sends "orderid", unless you configure PropertyNameCaseInsensitive, whereas Newtonsoft.Json matched case-insensitively by default; if you're behind ASP.NET Core specifically, this is less often a surprise, since the framework configures camel-casing and case-insensitive matching for you automatically. Comments and trailing commas in hand-written or legacy JSON configuration files throw a JsonException by default; enable JsonCommentHandling.Skip and AllowTrailingCommas explicitly if you need to tolerate them. Polymorphic $type-driven deserialization via TypeNameHandling.All has no equivalent — you must redesign the contract around [JsonPolymorphic]/[JsonDerivedType], which is a real design change, not a configuration flag, and is usually the single largest migration task in a codebase that leaned on it. JToken/JObject/JArray-based dynamic JSON manipulation, including SelectToken JSONPath queries, has no direct equivalent; JsonDocument/JsonElement covers navigation but not JSONPath query syntax, so code built around ad hoc document querying needs to be rewritten against JsonElement traversal or a typed model. And some Newtonsoft.Json limits and behaviors simply aren't configurable in System.Text.Json — it favors a smaller, more predictable set of options over Newtonsoft.Json's very large surface area, so a migration sometimes has to accept "no direct equivalent" for a rarely-used setting rather than find a one-line replacement.

What interviewers look for: specific, named gotchas rather than "some things are different," and recognition that the polymorphism and JSONPath gaps are architecture-level migration work, not configuration tweaks, which is exactly the kind of estimate a lead needs to get right before committing to a migration timeline.

  • Common mistakes: treating the migration as a find-and-replace of namespace imports and attribute names, then discovering case-sensitivity or comment-handling breaks integration tests weeks later.
  • Follow-up questions: how would you stage a migration in a large codebase to avoid a single high-risk, all-at-once cutover? What automated test would have caught a case-sensitivity regression before it reached production?

Q8 How do naming policies, casing and enum serialization typically go wrong across service boundaries?#

Short answer: Naming and enum mismatches are a contract problem disguised as a serialization bug: two services agree on a schema informally, one side changes a default (or an enum member), and the failure shows up as silent data loss or a runtime exception in whichever side is stricter — usually the System.Text.Json-based consumer, precisely because it doesn't guess.

Property naming policy is the most common source: System.Text.Json's default PropertyNamingPolicy is null, meaning it serializes properties using their exact declared C# name (typically PascalCase) unless you configure JsonNamingPolicy.CamelCase or a custom policy — and ASP.NET Core configures camelCase for you by default for controllers and minimal APIs, which means a service calling out to another team's API directly with JsonSerializer.Serialize and no explicit naming policy can send PascalCase where the receiving service expects camelCase, and get silent property-binding misses rather than an error, since unmatched JSON properties are ignored by default rather than rejected. Enums are the second common surprise: System.Text.Json serializes enums as their underlying numeric value by default, not their name, so OrderStatus.Cancelled becomes 3 on the wire unless you add a JsonStringEnumConverter; a consumer expecting the string name gets a number instead, and — worse — if the enum's members are ever reordered or a new value is inserted in the middle rather than appended, the numeric values silently shift and old serialized data or logs are misinterpreted against the new numbering. The fix for both is making the wire contract explicit rather than relying on either side's defaults: agree on and configure a naming policy consistently, and prefer string-based enum serialization (accepting the extra bytes) specifically because it survives enum reordering and is self-describing in logs and captured payloads.

What interviewers look for: identifying that these are contract-design problems, not serializer bugs, and knowing the specific, checkable default behaviors (PascalCase unless configured, numeric enums unless configured) that make the mismatch predictable once you know to look for it.

  • Common mistakes: discovering a naming mismatch only in an integration environment because local development used the same team's client and server, which happened to agree by convention rather than by explicit configuration.
  • Follow-up questions: what's the compatibility risk of switching an already-shipped API from numeric to string enum serialization? How would JsonStringEnumConverter behave for a value the reader doesn't recognize?

Q9 How do you handle circular references and shared references in a JSON object graph?#

Short answer: By default, System.Text.Json throws a JsonException the moment it detects a genuine reference cycle during serialization, because JSON has no native way to express "this points back to an object already being written" — you choose explicitly between preserving that structure with reference metadata (ReferenceHandler.Preserve) or discarding the back-reference to keep the JSON shape plain (ReferenceHandler.IgnoreCycles), and the two are not interchangeable.

ReferenceHandler.Preserve makes cycles and shared references round-trippable by writing $id on the first occurrence of a complex object and $ref on any later reference to that same object instance — collections get an additional $values wrapper to carry their metadata. This is the only option of the two that faithfully reconstructs the original object graph, including two properties that legitimately point at the very same instance, but it changes your wire format: a consumer that isn't also using System.Text.Json with Preserve enabled — a JavaScript client, a service on another platform — has no idea what $id/$ref mean and will see them as ordinary, confusing extra properties, so it's a strong fit for internal .NET-to-.NET serialization (caching a graph, an in-process queue payload) and a poor fit for a public API contract. ReferenceHandler.IgnoreCycles takes the opposite trade-off: on encountering a cycle, it sets the back-reference property to null instead of writing metadata, keeping the JSON shape plain and universally consumable, at the cost of silently losing that one piece of information from the serialized payload — an employee's Manager.DirectReports collection that circles back to the employee simply won't show that specific entry.

C#
var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.IgnoreCycles };
string json = JsonSerializer.Serialize(orgChart, options); // cyclical Manager/DirectReports nulled out

The right default for most public-facing APIs is neither: design the DTO to not contain a cycle in the first place — a flat ManagerId instead of a full Manager navigation property is usually both simpler and exactly what the client needed — and reserve Preserve/IgnoreCycles for internal serialization where reshaping the contract isn't practical.

What interviewers look for: knowing the concrete difference in wire format and interoperability between Preserve and IgnoreCycles, not just that "there's a setting for circular references," and the judgment to prefer redesigning the DTO over reaching for either option on a public contract.

  • Follow-up questions: why would Preserve's $id/$ref metadata be a problem for a public REST API even if every current consumer happens to be a .NET client? How does EF Core's navigation-property-heavy entity model make this problem more likely to show up by accident?

Q10 How do you profile and reduce JSON (de)serialization cost when it's a measurable share of request time?#

Short answer: Confirm serialization is actually the cost — with dotnet-trace or a targeted BenchmarkDotNet benchmark against representative payloads, not assumption — then attack it in order: remove unnecessary reflection with source generation, remove unnecessary allocations by streaming instead of materializing intermediate strings, and remove unnecessary data by shaping the payload to what the client needs.

Start by isolating the cost with a trace rather than guessing: it's common for "JSON is slow" to actually mean an EF Core query is materializing a much larger object graph than the payload needs, and the serializer is faithfully doing exactly the amount of work that oversized graph requires. Once serialization itself is confirmed as the cost, the changes in order of typical impact are: switch from the reflection default to source generation, which removes both a one-time reflection cost per type and ongoing metadata lookups; serialize directly to the response stream (Results.Json, HttpResponse.BodyWriter, or JsonSerializer.SerializeAsync against the response stream) instead of building an intermediate string, which avoids a redundant UTF-16 buffer and its GC pressure; reuse a single, cached JsonSerializerOptions instance — construction and first-use configuration aren't free, and instances are safe to share for reads once configured; and, only after those, consider narrowing the DTO itself, which reduces both serialization work and network transfer regardless of which serializer or settings you use. Measure again after each change rather than stacking all of them and hoping — source generation alone often closes most of the gap, and chasing further micro-optimizations on a payload that's already small and simple has a low ceiling compared to fixing an oversized query or a chatty API shape upstream of serialization entirely.

What interviewers look for: a measurement-driven order of operations rather than reflexively reaching for the most exotic optimization first, and the judgment to recognize when the real fix is upstream of the serializer (query shape, payload shape) rather than in it.

  • Follow-up questions: how would you tell, from a trace, whether time is spent in serialization CPU work versus waiting on the network to accept written bytes? When does narrowing a DTO stop being worth the added mapping code?

Quick-Fire Round#

QuestionAnswer
Default MaxDepth in System.Text.Json?64, matching Newtonsoft.Json's own default.
Default enum serialization format?Numeric value, not the member name, unless JsonStringEnumConverter is added.
Default property name matching?Case-sensitive, unless PropertyNameCaseInsensitive is set.
Attribute that opts a base type into polymorphism?[JsonPolymorphic], paired with [JsonDerivedType] on each derived type.
Default type discriminator property name?$type, customizable via TypeDiscriminatorPropertyName.
API for streaming a large JSON array element by element?JsonSerializer.DeserializeAsyncEnumerable<T>.
Option that writes $id/$ref/$values metadata?ReferenceHandler.Preserve.
Why can't Utf8JsonReader be stored in a field?It's a ref struct, restricted to the stack.
What does System.Text.Json refuse to support, by design, for security reasons?Open-ended $type-driven polymorphism like TypeNameHandling.All.

How to Prepare#

  • Know the concrete default-behavior differences from Newtonsoft.Json cold — case sensitivity, comments, enums, polymorphism — since these are the most common follow-up traps.
  • Practice writing a [JsonPolymorphic]/[JsonDerivedType] hierarchy and a minimal JsonConverter<T> from memory.
  • Be ready to explain MaxDepth in terms of the actual attack it prevents, not just "it's a limit."
  • Rehearse the measurement-first order of operations for reducing serialization cost, with source generation as the first lever, not the only one.
  • Have a real migration or contract-mismatch story ready — naming policy or enum drift across services is a very common real-world bug to have lived through.