Why are properties without a setter not serialized
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A common source of confusion in .NET APIs is seeing a property appear during serialization but fail during deserialization, or disappear entirely after a serializer change. Properties without setters sit at the center of this issue. The key is to understand that writing JSON and reading JSON are separate operations with different requirements.
Serialization and Deserialization Are Not Symmetric
When writing JSON, a serializer only needs to read a value, so a public getter is often enough. When reading JSON back into an object, the serializer needs a way to assign the value. Without a setter, assignment can still work, but only if there is constructor binding or an init path.
This difference explains many bugs that look inconsistent at first glance.
System.Text.Json Example with Constructor Binding
System.Text.Json supports immutable patterns if constructor parameters map to property names.
If constructor parameter names do not align with the JSON property names, deserialization can fail or produce default values.
Using init for Transport Models
init properties are writable only during initialization, which provides controlled mutability while keeping runtime behavior mostly immutable.
This style works well for API contracts because serializers can set values, but regular code cannot mutate them later by accident.
Newtonsoft.Json Behavior and Migration Risks
Newtonsoft.Json and System.Text.Json differ in defaults and extension points. A model that worked under one serializer may behave differently after migration. The safe migration workflow is:
- Compare serialized JSON snapshots for representative objects.
- Verify round-trip behavior for immutable and mutable types.
- Review custom converter and contract resolver assumptions.
Do not assume identical behavior across serializer libraries or major framework versions.
Domain Model Versus DTO Strategy
Trying to make one class satisfy every concern often creates tradeoffs between domain safety and transport convenience. A practical pattern is using separate DTOs for serialization boundaries.
Map between DTO and domain types at API edges. This keeps serialization concerns out of core business logic and preserves invariants.
Debugging Missing Read-Only Properties
When a property is not present where expected, check these items in order:
- Property visibility is public and not ignored by attributes.
- Serializer options and naming policies are what you think they are.
- Constructor binding names match payload names.
- Custom converters are not overriding default member handling.
This sequence usually identifies the root cause quickly.
Testing Contract Stability
Serializer behavior can change subtly during upgrades. Add explicit tests for both directions:
- Serialization shape test, verifying expected keys exist.
- Deserialization test for immutable models and constructor mapping.
Even simple snapshot tests catch many breaking changes before production rollouts.
Common Pitfalls
- Assuming getter-only properties always deserialize automatically.
- Mixing serializer libraries without validating behavior differences.
- Renaming constructor parameters and breaking immutable binding.
- Forcing mutable setters into domain models only to satisfy transport needs.
- Skipping serialization tests during framework upgrades.
Summary
- Setter-less properties are easy to serialize but may require explicit paths to deserialize.
- Constructor binding and
initare key tools for immutable-friendly contracts. - Serializer defaults differ across libraries, so migration requires verification.
- DTO mapping is often cleaner than weakening domain model invariants.
- Add contract tests to catch silent serialization behavior regressions.

