Your unit tests are green. The release build completes. The protected application launches.
Then an integration sends a perfectly valid payload, and a property comes back empty.
This is a frustrating class of bug because the source code can look completely correct. The failure appears only after the assembly has passed through the protection step.
Let’s approach it as a debugging exercise: reproduce the mismatch, separate wire names from CLR names, and add a test that checks the artifact you actually ship.
The same approach applies to JSON and XML models protected with Opaquer .NET Obfuscator. Its serialization guide identifies member renaming, contract metadata, and polymorphic type resolution as compatibility concerns.
- Start with a tiny reproduction Suppose your application accepts this payload: { "CustomerName": "Acme Corp", "CustomerId": 42 } Your DTO looks straightforward: public sealed class CustomerDto { public string CustomerName { get; set; } = ""; public int CustomerId { get; set; } } And your deserialization code is equally ordinary: using System.Text.Json;
var customer = JsonSerializer.Deserialize(payload);
The problem appears when the serializer’s naming contract depends on property names that the obfuscator changes.
Conceptually, the protected model might now resemble:
public sealed class a
{
public string b { get; set; } = "";
public int c { get; set; }
}
This is an illustration, not a prediction of Opaquer’s exact output.
The sender still transmits CustomerName. A name-based contract built from the renamed metadata may expect something else. Depending on serializer configuration, the unmatched value can leave the property at its default rather than produce an obvious exception.
That explains why “the request did not throw” is a weak success criterion.
- Write the failing assertion before changing settings The first useful test should use a fixed external payload and inspect the resulting values. For example, with xUnit: using System.Text.Json; using Xunit;
public sealed class CustomerContractTests
{
[Fact]
public void Reads_the_existing_customer_payload()
{
const string payload =
"{\"CustomerName\":\"Acme Corp\",\"CustomerId\":42}";
var customer =
JsonSerializer.Deserialize<CustomerDto>(payload);
Assert.NotNull(customer);
Assert.Equal("Acme Corp", customer!.CustomerName);
Assert.Equal(42, customer.CustomerId);
}
}
Run the equivalent verification against the protected release output, not just the development assembly.
The source guide specifically recommends exercising protected artifacts with external payload fixtures, round trips, and polymorphic cases.
The implementation of that test stage depends on your build layout. A small integration harness can invoke the protected application through its normal input/output boundary. If the test harness references DTO members directly, those references also need to remain compatible with the protected assembly.
The important requirement is that the code under test is the code you distribute.
- Make the wire names explicit Now remove the implicit dependency between the JSON field and the CLR property name: using System.Text.Json.Serialization;
public sealed class CustomerDto
{
[JsonPropertyName("CustomerName")]
public string CustomerName { get; set; } = "";
[JsonPropertyName("CustomerId")]
public int CustomerId { get; set; }
}
Notice that the attribute values preserve the existing payload names.
This is not the moment to switch CustomerName to customer_name unless you intend to change the external contract.
For Newtonsoft.Json, the equivalent declaration uses JsonProperty:
using Newtonsoft.Json;
public sealed class CustomerDto
{
[JsonProperty("CustomerName")]
public string CustomerName { get; set; } = "";
[JsonProperty("CustomerId")]
public int CustomerId { get; set; }
}
These attributes provide explicit serialized names. The Opaquer guide presents them as a way to permit property-symbol renaming while retaining the metadata the serializer needs. That relies on the relevant attributes and their values surviving protection correctly.
The principle is decoupling: the name used by your implementation does not have to determine the name used by the payload.
- Handle XML as a separate contract Do not assume that fixing JSON automatically fixes XML. An XML model can make its external names explicit too: using System.Xml.Serialization;
[XmlRoot("Customer")]
public sealed class CustomerXmlDto
{
[XmlElement("CustomerName")]
public string CustomerName { get; set; } = "";
[XmlAttribute("CustomerId")]
public int CustomerId { get; set; }
}
Its expected shape is:
Acme Corp
The attached guide includes XML contract attributes alongside JSON attributes in its selective-protection strategy.
Keep an XML fixture in the test suite and verify the names that matter: root elements, child elements, attributes, and any type information used by your model. A successful JSON test provides no evidence that these XML details remain intact.
- Don’t let a round trip give you false confidence Consider this test: Object → protected serializer → payload → protected deserializer → object It checks whether the application can read what it writes. But imagine both operations now agree on renamed fields. The round trip can succeed even though an external client still sends the original field names. Use two additional checks: • Input compatibility: deserialize a fixed payload supplied independently of the protected serializer. • Output compatibility: inspect the generated payload and verify its agreed field names and values. For JSON, an output assertion could look like this: var json = JsonSerializer.Serialize(new CustomerDto { CustomerName = "Acme Corp", CustomerId = 42 });
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
Assert.Equal(
"Acme Corp",
root.GetProperty("CustomerName").GetString());
Assert.Equal(
42,
root.GetProperty("CustomerId").GetInt32());
Checking the parsed structure avoids making the test depend unnecessarily on whitespace or property ordering.
This expands the source guide’s combination of round-trip tests and external-payload tests into a two-way compatibility check.
- Give polymorphic payloads their own test cases If a base model can deserialize into several concrete types, property names are only part of the contract. You must also verify how the payload selects the concrete type. The Opaquer guide discusses explicit System.Text.Json discriminators and Newtonsoft.Json type metadata as relevant cases. Whether renaming causes a failure depends on the actual mapping: a stable discriminator is different from a serialized CLR type name. For every supported subtype, test:
- A fixture containing the expected discriminator or type metadata.
- The concrete type created during deserialization.
- The values specific to that subtype.
- The corresponding outbound representation. If your configuration depends on CLR names, either preserve the necessary names or introduce an explicit mapping appropriate to that serializer. Do not assume that adding property attributes also solves type resolution.
- Apply the smallest protection exception that works Some projects cannot annotate every model immediately. Legacy DTOs, generated code, and shared libraries may require renaming exclusions instead. Opaquer’s guide describes selective exemptions for contract types and properties, while retaining protection for internal processing code. It also recommends storing protection rules in a repeatable project configuration. A practical migration strategy is: Model category Starting policy DTOs with implicit serialized names Preserve the names required by the contract DTOs with explicit serialized names Allow renaming only after protected-artifact tests pass Models using polymorphic type metadata Review and test type resolution separately Internal implementation code Apply protection, subject to its own runtime dependencies Avoid disabling renaming across the entire application merely because one DTO fails. Narrow the exception, document why it exists, and keep a regression test beside it. Turn the bug into a release gate The finished workflow should look like this: Compile ↓ Protect with Opaquer ↓ Exercise the protected artifact ↓ Verify inbound and outbound JSON/XML contracts ↓ Publish The source guide recommends testing the obfuscated application and integrating saved protection rules into CI/CD. That changes the acceptance criterion from “the protected executable runs” to “the protected executable still speaks the correct protocol.” For serialization-heavy applications, that is the difference between a successful build and a shippable release.
Top comments (0)