serialization
properties
getters and setters
data encapsulation
object-oriented programming

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.

csharp
1using System;
2using System.Text.Json;
3
4public sealed class UserProfile
5{
6    public string Name { get; }
7    public int Age { get; }
8
9    public UserProfile(string name, int age)
10    {
11        Name = name;
12        Age = age;
13    }
14}
15
16var value = new UserProfile("Mina", 31);
17var json = JsonSerializer.Serialize(value);
18Console.WriteLine(json);
19
20var roundTrip = JsonSerializer.Deserialize<UserProfile>(json);
21Console.WriteLine($"{roundTrip?.Name} {roundTrip?.Age}");

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.

csharp
1public sealed class ProductDto
2{
3    public string Sku { get; init; } = "";
4    public decimal Price { get; init; }
5}

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.

csharp
1public sealed class OrderDto
2{
3    public Guid Id { get; set; }
4    public string Status { get; set; } = "";
5}
6
7public sealed class Order
8{
9    public Guid Id { get; }
10    public string Status { get; private set; }
11
12    public Order(Guid id, string status)
13    {
14        Id = id;
15        Status = status;
16    }
17}

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.
csharp
// Example test idea
// var json = JsonSerializer.Serialize(sample);
// Assert.Contains("Name", json);

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 init are 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.

Course illustration
Course illustration

All Rights Reserved.