Skip to main content
ANVISoftware Solutions
Lesson 4 of 18Beginner16 min

JSON and Serialisation

By the end of this lesson

Control how objects are converted to and from JSON.

Serialisation is the conversion between an object in memory and text on the wire. Your C# type becomes JSON on the way out, and incoming JSON becomes a C# object on the way in. In ASP.NET Core that work is done by System.Text.Json, and by default it happens without you writing a line of it.

That convenience hides a contract. The names, the letter case, the way a date is written, whether a missing value appears as null or vanishes — every one of those is part of what callers depend on. If you do not decide them, the defaults decide for you, and they change when your C# changes.

Five decisions worth making once, in one place, for the whole API:

Property naming
Use camelCase in JSON — fullName, not FullName. ASP.NET Core's web defaults already do this, and incoming names are matched case-insensitively. Setting it explicitly records the decision rather than inheriting it.
Nulls
Decide whether an absent value is written as null or omitted entirely. Both are workable. Omitting makes payloads smaller; writing null makes the shape predictable, which is easier for callers to code against.
Enums
Write them as strings, not numbers. "Active" is readable in a log, meaningful in a client, and does not depend on the order your enum members happen to be declared in.
Dates and times
Use ISO 8601. A moment in time is a DateTimeOffset so the offset from UTC travels with it. A calendar date with no time, such as a start date, is a DateOnly and serialises as 2024-04-17.
Numbers
Money is decimal in C#, and JSON writes it as a number. Very large integers are worth sending as strings, because some clients parse every JSON number as a double and lose precision.
Serialisation policy in one place, and the type it applies to
C#
using System.Text.Json;
using System.Text.Json.Serialization;

// Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.Never;
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

// Contracts/EmployeeResponse.cs
public enum EmploymentStatus { Active, OnLeave, Left }

public sealed record EmployeeResponse(
    int Id,
    string FullName,
    string Email,
    EmploymentStatus Status,
    DateOnly StartDate,
    DateTimeOffset CreatedAt,
    int? ManagerId);
  • ConfigureHttpJsonOptions sets the policy for minimal API endpoints. If you use controllers, the equivalent is builder.Services.AddControllers().AddJsonOptions(...). Setting it in both places is worth doing in a project that mixes them.
  • PropertyNamingPolicy.CamelCase turns FullName into fullName. This is already the default for web applications; stating it means a future change to the defaults cannot quietly alter your contract.
  • JsonIgnoreCondition.Never writes null for absent values instead of omitting the property. Callers then see managerId on every employee, which is easier to model than a property that sometimes is not there.
  • JsonStringEnumConverter writes Status as "OnLeave" rather than 1. Without it, System.Text.Json writes the numeric value.
  • DateOnly for StartDate and DateTimeOffset for CreatedAt is the distinction that saves the most confusion later: one is a date on a calendar, the other is a specific moment somewhere in the world.
What a caller receives
JSON
{
  "id": 42,
  "fullName": "Asha Menon",
  "email": "asha.menon@example.com",
  "status": "OnLeave",
  "startDate": "2024-04-17",
  "createdAt": "2024-04-17T09:31:44+05:30",
  "managerId": null
}
  • Every property is camelCase, so a JavaScript client reads employee.fullName without a mapping layer.
  • status is a word. A client can compare it to "OnLeave" and a support engineer can read it in a log without a lookup table.
  • startDate has no time and no zone, because a start date does not have one. Sending it as a full timestamp invites a client in another zone to display the day before.
  • createdAt carries +05:30, so the moment is unambiguous. A timestamp with no offset is a guess about which clock it came from.
  • managerId is present and null rather than missing, because DefaultIgnoreCondition.Never was chosen. The caller can rely on the property existing.

Summary

  • Serialisation turns your objects into the contract, so its settings are contract decisions
  • Set naming, null handling, enum format and date format once for the whole API
  • camelCase names, enums as strings, ISO 8601 dates, and DateTimeOffset for moments in time
  • The serialiser writes every public property it finds, which is how internal fields leak
  • Renaming a C# property renames the JSON field and breaks callers unless you pin the name

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Think about it

Why a string beats a number

Your API sends employment status as an enum. Argue for sending "OnLeave" rather than 1, and describe the specific failure the number invites.

Show solution

A string is self-describing. Anyone reading a response, a log line or a support ticket knows what it means, and a new client needs no mapping table to consume it.

The specific failure is silent renumbering. Enum members without explicit values are numbered by declaration order, so inserting OnLeave between Active and Left changes what 1 means. Every stored or hardcoded number in every client now points at a different status, and nothing raises an error — the data is wrong and looks fine.

Assigning explicit numeric values to every member removes that hazard, so numbers are not indefensible. Strings still read better and cost almost nothing, which is why they are the more common choice.

Try it yourself

Inspect your own output

Add the JSON options above to an API you have, then call one endpoint and read the raw response rather than a formatted view of it.

Check four things: is every name camelCase, are enums words, do timestamps carry an offset, and is there any property in the response you did not intend to publish?

Show solution

The fourth check is the one that finds problems. Reading the raw body is different from reading the model you thought you were returning, and the gap between them is where leaked fields live.

If you found an unintended property, resist adding [JsonIgnore] to it and moving on. That hides one field today and the next one added to the entity will be exposed again. A dedicated response type fixes the category rather than the instance.

Saved in this browser only.