Testing & Reliable Design · Lesson 18

Contract checks across a JSON boundary

Exercise the real local serializer while keeping external infrastructure outside the claim.

Define the observable contract

An integration boundary connects components with different contracts. This lesson exercises the real local System.Text.Json serializer and a simple Request record, rather than a fake serializer that always returns the input. That is meaningful evidence for field mapping and value reconstruction in this process. It is deliberately narrower than an end-to-end remote API test. No HTTP server, database, authentication, durable file, or other machine participates, so success here must not be described as proof that all of those external boundaries behave correctly.

Walk through the real program

The request contains code A1 and integer quantity two. The camel-case policy produces the exact observed JSON {"code":"A1","quantity":2}, printed on the first line. Reading the same text with the same options reconstructs an equal record and prints roundtrip:True. The deliberately malformed text consisting of an opening brace throws JsonException and prints representation:rejected. These three observations cover one valid mapping and one representation failure. They do not validate required-member presence or the business acceptability of every possible quantity in arbitrary incoming documents.

Why the design is testable

A round-trip check alone can miss a shared mistake. If both writer and reader use an unintended field name, they may agree with each other while violating an external format. Retain an independent expected wire shape or a fixture from the documented contract as well as a restored-value comparison. Check member names and JSON kinds rather than assuming two similar strings are semantically identical. JSON objects do not assign domain meaning to property order, even though this concrete serializer/model combination emits a predictable string in the prepared example.

A boundary that matters

The naming policy is not a general case-insensitive parser, and a successful deserialize is not complete domain validation. A null root can produce a null record, while missing or unknown members have serializer-specific behavior. Decide presence, version, and range rules at the application boundary. Do not replace System.Text.Json with JsonUtility, another package, or a remote service and assume this check establishes identical features. Each adapter deserves evidence for its actual mapping and failure policy, especially when a supposedly compatible refactor changes defaults.

Practice: isolate one behavior

First change the request quantity to four and predict the matching number in JSON while roundtrip remains true. Then change the naming policy consistently in both directions and inspect the altered wire names. Hint: a successful value round trip can remain true while the external contract changed. Add an independently written expected JSON fixture or parsed member assertions so that a wrong naming-policy change would be detected. Keep a malformed-input case separate from valid JSON containing a domain-invalid quantity.

Practice: challenge another assumption

For another exercise, read a prepared document with code A1 but no quantity and inspect the restored value before applying an explicit domain validator. Hint: missing input and a legitimate zero can require a presence policy beyond a numeric default. Add a version field and define how an unsupported version is rejected. Then outline a separate real API integration test using the actual transport adapter, while keeping these small local mapping checks fast and portable. State exactly which additional external behavior that broader test would establish.

Review the evidence

The baseline tests a genuine local serializer boundary, not an invented all-purpose application simulation. Its three lines should be interpreted according to that scope. Strong boundary evidence combines independent format expectations, restored domain values, and deliberate failure cases, while retaining separate tests for external infrastructure and complete user workflows. This precision prevents both underclaiming useful local integration evidence and overclaiming an entire system from one successful serialization round trip. Review the contract at each boundary instead of treating integration as one undifferentiated testing category. Keep a captured external fixture under version control when interoperability matters, and validate it independently instead of regenerating the expected document with the writer under examination.

  • The code uses the real local serializer and explicitly configured names.
  • A round trip alone can allow the writer and reader to share a mistake.
  • This program does not exercise remote transport, storage, or authentication.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var request = new Request("A1", 2);
string json = JsonSerializer.Serialize(request, options);
Console.WriteLine(json);
var restored = JsonSerializer.Deserialize<Request>(json, options);
Console.WriteLine($"roundtrip:{restored == request}");
try { _ = JsonSerializer.Deserialize<Request>("{", options); }
catch (JsonException) { Console.WriteLine("representation:rejected"); }
record Request(string Code, int Quantity);

Expected output

{"code":"A1","quantity":2}
roundtrip:True
representation:rejected
Try this
A local serializer check is real evidence for that boundary, not proof of a remote API or durable store.

Further reading