Pragmatic JSON handling in .NET
JSON is the default wire format for most of my services: HTTP APIs, queue messages, and configuration. The tooling in modern .NET is good, but it’s still easy to end up with fragile models or painful migrations. Here’s how I keep it boring and predictable.
1. Treat JSON contracts as public APIs
Even if a message is “internal”, I treat its JSON shape as an API contract:
- Prefer stable names over clever abbreviations.
- Avoid renaming or removing fields once they’re live.
- Add new fields as optional with sensible defaults.
2. Use dedicated DTOs, not your EF entities
I almost never serialize EF entities directly. Instead I use explicit DTOs:
- Entities can evolve for persistence without breaking wire contracts.
- DTOs can be tuned for clients (flattened, renamed, grouped).
- Versioning and compatibility logic lives in mapping code, not “magic”.
3. System.Text.Json with explicit options
I stick with System.Text.Json and configure options centrally:
- Case-insensitive property matching for input.
- Consistent naming policy (camelCase on the wire).
- Converters for tricky types (enums, DateTime, value objects).
4. Plan for “unknown” data
When integrating with external systems, I often include a catch-all:
- An
IDictionary<string, JsonElement>for extensions. - A raw JSON payload column in SQL for debugging and replays.
That way, if a partner adds a field, my service doesn’t instantly fall over.
5. Log the right amount of JSON
Logging full payloads in production is tempting but dangerous (PII, log volume). I prefer:
- Logging only keys needed to debug (IDs, a few key fields).
- Redacting obvious sensitive fields.
- Sampling full payload logs in non-prod environments instead.