Three outcomes, one underlying mismatch
No exception-just missing values. JSON deserializers often “skip keys they can’t map.”
Polymorphic payloads fail when the discriminator can no longer be resolved.
XML contracts can hinge on type names; renaming changes those identifiers.
The root pattern: the runtime looks up members/types by string identifiers stored in assembly metadata. If obfuscation rewrites those identifiers but serializers still expect the original “wire names”, your contract stops matching.
Wire names vs. symbol names
Think of every DTO as governed by two separate naming domains:
Keys and element names (and polymorphic discriminators like "$type" / xsi:type) are part of the payload shape. Your customers and integrations depend on these strings.
The member and type identifiers stored in your assembly metadata are what serializers consult when you don’t explicitly pin names.
If you rename the symbol contract but the serializer still tries to find the original wire strings, mapping fails. Sometimes it fails loudly; sometimes it fails silently.
{
"$type": "card",
"payment_ref": "PAY-20391",
"Currency": "EUR",
"last_four": "4242",
"ExpiryMonth": 12
}
Keep the wire stable: pin or shield
Pin it with attributes
Move serializer-visible names into attribute metadata so renaming can’t break them.
[JsonPropertyName]/[JsonProperty][XmlRoot]/[XmlElement]/[XmlAttribute]/[XmlType]- Explicit polymorphic discriminators (avoid “default to type name” behavior)
[DataMember(Name = "...")]
Shield it by freezing symbols
For legacy DTOs where the wire format rides on bare member names, exclude those types (or namespaces) from renaming. The symbol contract remains identical, so the serializer still finds what it expects.
Polymorphism is the most fragile surface
Discriminator names are also “wire strings”. If they’re derived from type names, obfuscation can break resolution.
Prefer explicit discriminator strings in JsonDerivedType.
Avoid brittle TypeNameHandling resolution; constrain using a strict binder/whitelist approach.
Pin xsi:type with [XmlType] or shield the type names.
Verify the obfuscated artifact (not the source)
The simplest way to avoid “works in smoke tests, fails in production” is to run a contract round-trip against the protected binary.
- 1 Commit golden payloads (JSON and XML), including each polymorphic subtype.
- 2 Deserialize and assert every property binds to a non-default value.
- 3 Round-trip both directions, and compare key sets (format-insensitive).
- 4 Test the obfuscated artifact produced by your pipeline.
- 5 Fail on silence: a null where a value belongs should break CI.
[Fact]
public void Card_payment_binds_every_wire_key()
{
var dto = JsonSerializer.Deserialize<CardPaymentDto>(Golden.CardPayment);
Assert.Equal("PAY-20391", dto.PaymentRef); // pinned by attribute
Assert.Equal("EUR", dto.Currency); // pinned/shielded symbol
Assert.Equal(12, dto.ExpiryMonth); // pinned/shielded symbol
Assert.Equal("4242", dto.LastFour); // pinned by attribute
}
Questions from the field
Is it safer to disable renaming across my entire assembly? +
Do [DataContract] and [DataMember] attributes make members rename-safe? +
[DataMember(Name = "invoice_total")]). If you leave the name unspecified, the serializer still relies on the member symbol name-so you must shield that member instead.
Can string encryption corrupt JSON keys or XML names? +
How do I catch broken contracts before release? +
Contract-preserving obfuscation isn’t a compromise-it’s the only craft that ships reliably.
Pin wire names, shield legacy DTO symbols, then test the protected binary. Your customers keep getting the same payload shape-your attackers don’t get readable code.